[#306] In APE buckets forbid canned acl except private
Some checks failed
/ DCO (pull_request) Successful in 2m50s
/ Vulncheck (pull_request) Failing after 3m15s
/ Builds (1.20) (pull_request) Successful in 3m39s
/ Builds (1.21) (pull_request) Successful in 3m41s
/ Lint (pull_request) Successful in 5m48s
/ Tests (1.20) (pull_request) Successful in 4m0s
/ Tests (1.21) (pull_request) Successful in 3m53s
Some checks failed
/ DCO (pull_request) Successful in 2m50s
/ Vulncheck (pull_request) Failing after 3m15s
/ Builds (1.20) (pull_request) Successful in 3m39s
/ Builds (1.21) (pull_request) Successful in 3m41s
/ Lint (pull_request) Successful in 5m48s
/ Tests (1.20) (pull_request) Successful in 4m0s
/ Tests (1.21) (pull_request) Successful in 3m53s
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
62cc5a04a7
commit
80c7b73eb9
9 changed files with 290 additions and 110 deletions
|
@ -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 {
|
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()
|
ownerDisplayName := bktInfo.Owner.EncodeToString()
|
||||||
ownerEncodedID := ownerDisplayName
|
ownerEncodedID := ownerDisplayName
|
||||||
|
|
||||||
|
@ -308,26 +334,6 @@ func (h *handler) encodeBucketCannedACL(ctx context.Context, bktInfo *data.Bucke
|
||||||
Permission: aclFullControl,
|
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
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -513,19 +519,17 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apeEnabled := bktInfo.APEEnabled
|
|
||||||
|
|
||||||
if !apeEnabled {
|
|
||||||
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
apeEnabled = len(settings.CannedACL) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if apeEnabled {
|
if bktInfo.APEEnabled || len(settings.CannedACL) != 0 {
|
||||||
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
if err = middleware.EncodeToResponse(w, h.encodePrivateCannedACL(ctx, bktInfo, settings)); err != nil {
|
||||||
|
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -543,7 +547,7 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
objInfo, err := h.obj.GetObjectInfo(ctx, prm)
|
objInfo, err := h.obj.GetObjectInfo(ctx, prm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logAndSendError(w, "could not object info", reqInfo, err)
|
h.logAndSendError(w, "could not get object info", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1329,55 +1329,96 @@ func TestPutBucketAPE(t *testing.T) {
|
||||||
require.Len(t, chains, 2)
|
require.Len(t, chains, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPutBucketObjectACLErrorAPE(t *testing.T) {
|
func TestPutObjectACLErrorAPE(t *testing.T) {
|
||||||
hc := prepareHandlerContext(t)
|
hc := prepareHandlerContext(t)
|
||||||
bktName, objName := "bucket-for-acl-ape", "object"
|
bktName, objName := "bucket-for-acl-ape", "object"
|
||||||
|
|
||||||
info := createBucket(hc, bktName)
|
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{}
|
aclBody := &AccessControlPolicy{}
|
||||||
putBucketACLAssertS3Error(hc, bktName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported)
|
|
||||||
|
|
||||||
putObjectACLAssertS3Error(hc, bktName, objName, 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)
|
hc := prepareHandlerContext(t)
|
||||||
bktName := "bucket-for-acl-ape"
|
bktName := "bucket-for-acl-ape"
|
||||||
|
|
||||||
info := createBucket(hc, bktName)
|
info := createBucket(hc, bktName)
|
||||||
|
|
||||||
|
aclBody := &AccessControlPolicy{}
|
||||||
|
putBucketACLAssertS3Error(hc, bktName, info.Box, nil, aclBody, s3errors.ErrAccessControlListNotSupported)
|
||||||
|
|
||||||
aclRes := getBucketACL(hc, bktName)
|
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})
|
putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLPrivate})
|
||||||
aclRes = getBucketACL(hc, bktName)
|
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})
|
putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLReadOnly})
|
||||||
aclRes = getBucketACL(hc, bktName)
|
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})
|
putBucketACL(hc, bktName, info.Box, map[string]string{api.AmzACL: basicACLPublic})
|
||||||
aclRes = getBucketACL(hc, bktName)
|
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) {
|
func checkPrivateACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
||||||
checkBucketACLOwner(t, aclRes, ownerKey, 1)
|
checkACLOwner(t, aclRes, ownerKey, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPublicReadBucketACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
func checkPublicReadACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
||||||
checkBucketACLOwner(t, aclRes, ownerKey, 2)
|
checkACLOwner(t, aclRes, ownerKey, 2)
|
||||||
|
|
||||||
require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI)
|
require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI)
|
||||||
require.Equal(t, aclRead, aclRes.AccessControlList[1].Permission)
|
require.Equal(t, aclRead, aclRes.AccessControlList[1].Permission)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPublicReadWriteBucketACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
func checkPublicReadWriteACL(t *testing.T, aclRes *AccessControlPolicy, ownerKey *keys.PublicKey) {
|
||||||
checkBucketACLOwner(t, aclRes, ownerKey, 3)
|
checkACLOwner(t, aclRes, ownerKey, 3)
|
||||||
|
|
||||||
require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI)
|
require.Equal(t, allUsersGroup, aclRes.AccessControlList[1].Grantee.URI)
|
||||||
require.Equal(t, aclWrite, aclRes.AccessControlList[1].Permission)
|
require.Equal(t, aclWrite, aclRes.AccessControlList[1].Permission)
|
||||||
|
@ -1386,7 +1427,7 @@ func checkPublicReadWriteBucketACL(t *testing.T, aclRes *AccessControlPolicy, ow
|
||||||
require.Equal(t, aclRead, aclRes.AccessControlList[2].Permission)
|
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())
|
ownerIDStr := hex.EncodeToString(ownerKey.Bytes())
|
||||||
ownerNameStr := ownerKey.Address()
|
ownerNameStr := ownerKey.Address()
|
||||||
|
|
||||||
|
@ -1661,9 +1702,12 @@ func putObjectACLBase(hc *handlerContext, bktName, objName string, box *accessbo
|
||||||
return w
|
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)
|
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 {
|
func getObjectACLBase(hc *handlerContext, bktName, objName string) *httptest.ResponseRecorder {
|
||||||
|
@ -1671,3 +1715,29 @@ func getObjectACLBase(hc *handlerContext, bktName, objName string) *httptest.Res
|
||||||
hc.Handler().GetObjectACLHandler(w, r)
|
hc.Handler().GetObjectACLHandler(w, r)
|
||||||
return w
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx = r.Context()
|
ctx = r.Context()
|
||||||
reqInfo = middleware.GetReqInfo(ctx)
|
reqInfo = middleware.GetReqInfo(ctx)
|
||||||
|
|
||||||
containsACL = containsACLHeaders(r)
|
cannedACLStatus = aclHeadersStatus(r)
|
||||||
)
|
)
|
||||||
|
|
||||||
src := r.Header.Get(api.AmzCopySource)
|
src := r.Header.Get(api.AmzCopySource)
|
||||||
|
@ -93,7 +93,14 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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 {
|
if sessionTokenEACL, err = getSessionTokenSetEACL(ctx); err != nil {
|
||||||
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
||||||
return
|
return
|
||||||
|
@ -232,7 +239,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if containsACL {
|
if needUpdateEACLTable {
|
||||||
newEaclTable, err := h.getNewEAclTable(r, dstBktInfo, dstObjInfo)
|
newEaclTable, err := h.getNewEAclTable(r, dstBktInfo, dstObjInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
||||||
|
|
|
@ -22,6 +22,7 @@ type CopyMeta struct {
|
||||||
Tags map[string]string
|
Tags map[string]string
|
||||||
MetadataDirective string
|
MetadataDirective string
|
||||||
Metadata map[string]string
|
Metadata map[string]string
|
||||||
|
Headers map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCopyWithTaggingDirective(t *testing.T) {
|
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())
|
r.Header.Set(api.AmzTagging, tagsQuery.Encode())
|
||||||
|
|
||||||
|
for key, val := range copyMeta.Headers {
|
||||||
|
r.Header.Set(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
hc.Handler().CopyObjectHandler(w, r)
|
hc.Handler().CopyObjectHandler(w, r)
|
||||||
assertStatus(hc.t, w, statusCode)
|
assertStatus(hc.t, w, statusCode)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
"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"
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||||
"github.com/stretchr/testify/require"
|
"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 {
|
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 {
|
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)
|
w, r := prepareTestRequest(hc, bktName, objName, nil)
|
||||||
if encrypted {
|
if encrypted {
|
||||||
setEncryptHeaders(r)
|
setEncryptHeaders(r)
|
||||||
}
|
}
|
||||||
setHeaders(r, headers)
|
setHeaders(r, headers)
|
||||||
hc.Handler().CreateMultipartUploadHandler(w, r)
|
hc.Handler().CreateMultipartUploadHandler(w, r)
|
||||||
multipartInitInfo := &InitiateMultipartUploadResponse{}
|
return w
|
||||||
readResponse(hc.t, w, http.StatusOK, multipartInitInfo)
|
|
||||||
|
|
||||||
return multipartInitInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func completeMultipartUpload(hc *handlerContext, bktName, objName, uploadID string, partsETags []string) {
|
func completeMultipartUpload(hc *handlerContext, bktName, objName, uploadID string, partsETags []string) {
|
||||||
|
|
|
@ -103,6 +103,9 @@ const (
|
||||||
|
|
||||||
func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
reqInfo := middleware.GetReqInfo(r.Context())
|
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)
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -110,8 +113,17 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadID := uuid.New()
|
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
||||||
additional := []zap.Field{zap.String("uploadID", uploadID.String())}
|
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{
|
p := &layer.CreateMultipartParams{
|
||||||
Info: &layer.UploadInfoParams{
|
Info: &layer.UploadInfoParams{
|
||||||
|
@ -122,7 +134,8 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
|
||||||
Data: &layer.UploadData{},
|
Data: &layer.UploadData{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if containsACLHeaders(r) {
|
needUpdateEACLTable := !(apeEnabled || cannedACLStatus == aclStatusNo)
|
||||||
|
if needUpdateEACLTable {
|
||||||
key, err := h.bearerTokenIssuerKey(r.Context())
|
key, err := h.bearerTokenIssuerKey(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logAndSendError(w, "couldn't get gate key", reqInfo, err, additional...)
|
h.logAndSendError(w, "couldn't get gate key", reqInfo, err, additional...)
|
||||||
|
|
|
@ -186,12 +186,31 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
err error
|
err error
|
||||||
newEaclTable *eacl.Table
|
newEaclTable *eacl.Table
|
||||||
sessionTokenEACL *session.Container
|
sessionTokenEACL *session.Container
|
||||||
containsACL = containsACLHeaders(r)
|
cannedACLStatus = aclHeadersStatus(r)
|
||||||
ctx = r.Context()
|
ctx = r.Context()
|
||||||
reqInfo = middleware.GetReqInfo(ctx)
|
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 {
|
if sessionTokenEACL, err = getSessionTokenSetEACL(r.Context()); err != nil {
|
||||||
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
||||||
return
|
return
|
||||||
|
@ -204,12 +223,6 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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)
|
metadata := parseMetadata(r)
|
||||||
if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 {
|
if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 {
|
||||||
metadata[api.ContentType] = contentType
|
metadata[api.ContentType] = contentType
|
||||||
|
@ -261,12 +274,6 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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)
|
params.Lock, err = formObjectLock(ctx, bktInfo, settings.LockConfiguration, r.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logAndSendError(w, "could not form object lock", reqInfo, err)
|
h.logAndSendError(w, "could not form object lock", reqInfo, err)
|
||||||
|
@ -292,7 +299,7 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if containsACL {
|
if needUpdateEACLTable {
|
||||||
if newEaclTable, err = h.getNewEAclTable(r, bktInfo, objInfo); err != nil {
|
if newEaclTable, err = h.getNewEAclTable(r, bktInfo, objInfo); err != nil {
|
||||||
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
||||||
return
|
return
|
||||||
|
@ -465,7 +472,7 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx = r.Context()
|
ctx = r.Context()
|
||||||
reqInfo = middleware.GetReqInfo(ctx)
|
reqInfo = middleware.GetReqInfo(ctx)
|
||||||
metadata = make(map[string]string)
|
metadata = make(map[string]string)
|
||||||
containsACL = containsACLHeaders(r)
|
cannedACLStatus = aclHeadersStatus(r)
|
||||||
)
|
)
|
||||||
|
|
||||||
policy, err := checkPostPolicy(r, reqInfo, metadata)
|
policy, err := checkPostPolicy(r, reqInfo, metadata)
|
||||||
|
@ -483,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 {
|
if sessionTokenEACL, err = getSessionTokenSetEACL(ctx); err != nil {
|
||||||
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
||||||
return
|
return
|
||||||
|
@ -510,12 +536,6 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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{
|
params := &layer.PutObjectParams{
|
||||||
BktInfo: bktInfo,
|
BktInfo: bktInfo,
|
||||||
Object: reqInfo.ObjectName,
|
Object: reqInfo.ObjectName,
|
||||||
|
@ -582,9 +602,7 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings, err := h.obj.GetBucketSettings(ctx, bktInfo); err != nil {
|
if settings.VersioningEnabled() {
|
||||||
h.reqLogger(ctx).Warn(logs.CouldntGetBucketVersioning, zap.String("bucket name", reqInfo.BucketName), zap.Error(err))
|
|
||||||
} else if settings.VersioningEnabled() {
|
|
||||||
w.Header().Set(api.AmzVersionID, objInfo.VersionID())
|
w.Header().Set(api.AmzVersionID, objInfo.VersionID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -680,9 +698,33 @@ func checkPostPolicy(r *http.Request, reqInfo *middleware.ReqInfo, metadata map[
|
||||||
return policy, nil
|
return policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsACLHeaders(r *http.Request) bool {
|
type aclStatus int
|
||||||
return r.Header.Get(api.AmzACL) != "" || r.Header.Get(api.AmzGrantRead) != "" ||
|
|
||||||
r.Header.Get(api.AmzGrantFullControl) != "" || r.Header.Get(api.AmzGrantWrite) != ""
|
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) {
|
func (h *handler) getNewEAclTable(r *http.Request, bktInfo *data.BucketInfo, objInfo *data.ObjectInfo) (*eacl.Table, error) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
||||||
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
v2container "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
|
||||||
objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
objectv2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
"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 {
|
if obj, ok := t.objects[sAddr]; ok {
|
||||||
owner := getBearerOwner(ctx)
|
owner := getBearerOwner(ctx)
|
||||||
if !t.checkAccess(prm.Container, owner, eacl.OperationGet) {
|
if !t.checkAccess(prm.Container, owner, eacl.OperationGet, obj) {
|
||||||
return nil, ErrAccessDenied
|
return nil, ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,9 +323,9 @@ func (t *TestFrostFS) DeleteObject(ctx context.Context, prm PrmObjectDelete) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := t.objects[addr.EncodeToString()]; ok {
|
if obj, ok := t.objects[addr.EncodeToString()]; ok {
|
||||||
owner := getBearerOwner(ctx)
|
owner := getBearerOwner(ctx)
|
||||||
if !t.checkAccess(prm.Container, owner, eacl.OperationDelete) {
|
if !t.checkAccess(prm.Container, owner, eacl.OperationDelete, obj) {
|
||||||
return ErrAccessDenied
|
return ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,7 +377,7 @@ func (t *TestFrostFS) ContainerEACL(_ context.Context, prm PrmContainerEACL) (*e
|
||||||
return table, nil
|
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()]
|
cnr, ok := t.containers[cnrID.EncodeToString()]
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
|
@ -392,19 +393,48 @@ func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID, op eacl.Operation
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rec := range table.Records() {
|
for _, rec := range table.Records() {
|
||||||
if rec.Operation() == op && len(rec.Filters()) == 0 {
|
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() {
|
for _, trgt := range rec.Targets() {
|
||||||
if trgt.Role() == eacl.RoleOthers {
|
if trgt.Role() == eacl.RoleOthers {
|
||||||
return rec.Action() == eacl.ActionAllow
|
return true
|
||||||
}
|
}
|
||||||
var targetOwner user.ID
|
var targetOwner user.ID
|
||||||
for _, pk := range eacl.TargetECDSAKeys(&trgt) {
|
for _, pk := range eacl.TargetECDSAKeys(&trgt) {
|
||||||
user.IDFromKey(&targetOwner, *pk)
|
user.IDFromKey(&targetOwner, *pk)
|
||||||
if targetOwner.Equals(owner) {
|
if targetOwner.Equals(owner) {
|
||||||
return rec.Action() == eacl.ActionAllow
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,6 @@ const (
|
||||||
SomeACLNotFullyMapped = "some acl not fully mapped" // Warn in ../../api/handler/acl.go
|
SomeACLNotFullyMapped = "some acl not fully mapped" // Warn in ../../api/handler/acl.go
|
||||||
CouldntDeleteObject = "couldn't delete object" // Error in ../../api/layer/layer.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
|
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
|
BucketIsCreated = "bucket is created" // Info in ../../api/handler/put.go
|
||||||
CouldntDeleteNotificationConfigurationObject = "couldn't delete notification configuration object" // Error in ../../api/layer/notifications.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
|
CouldNotParseContainerObjectLockEnabledAttribute = "could not parse container object lock enabled attribute" // Error in ../../api/layer/container.go
|
||||||
|
|
Loading…
Reference in a new issue