[#49] Add basic ACL translation

Implement functions:
GetBucketACL, PutBucketACL, GetObjectACL,
PutObjectACL, GetBucketPolicy, PutBucketPolicy

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
support/v0.25
Denis Kirillov 2021-07-21 14:59:46 +03:00
parent b1c6629b10
commit efe11c271f
10 changed files with 2046 additions and 100 deletions

View File

@ -448,6 +448,14 @@ $ aws s3api delete-object --bucket %BUCKET_NAME --key %FILE_NAME
Reference:
* [AWS S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/s3-api.pdf)
### Limitations
#### ACL
For now there are some restrictions:
* [Bucket policy](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-policies.html)
support only one `Principal` (type `AWS`) per `Statement`. To refer all users use `"AWS": "*"`
* 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)
### Object
| Method | Status |
@ -469,8 +477,8 @@ Reference:
| Method | Status |
| ------------------------- | ----------------------- |
| GetObjectAcl | Unsupported |
| PutObjectAcl | Unsupported |
| GetObjectAcl | Supported |
| PutObjectAcl | Supported |
#### Locking
@ -540,8 +548,8 @@ See also `GetObject` and other method parameters.
| Method | Status |
| ------------------------- | ----------------------- |
| GetBucketAcl | Unsupported |
| PutBucketAcl | Unsupported |
| GetBucketAcl | Supported |
| PutBucketAcl | Supported |
#### Analytics
@ -630,11 +638,11 @@ See also `GetObject` and other method parameters.
| DeleteBucketPolicy | Unsupported |
| DeleteBucketReplication | Unsupported |
| DeletePublicAccessBlock | Unsupported |
| GetBucketPolicy | Unsupported |
| GetBucketPolicy | Supported |
| GetBucketPolicyStatus | Unsupported |
| GetBucketReplication | Unsupported |
| PostPolicyBucket | Unsupported, non-standard? |
| PutBucketPolicy | Unsupported |
| PutBucketPolicy | Supported |
| PutBucketReplication | Unsupported |
#### Request payment

View File

@ -298,6 +298,7 @@ const (
ErrEvaluatorInvalidTimestampFormatPatternSymbol
ErrEvaluatorBindingDoesNotExist
ErrMissingHeaders
ErrInvalidArgument
ErrInvalidColumnIndex
ErrAdminConfigNotificationTargetsFailed
@ -1838,6 +1839,12 @@ var errorCodes = errorCodeMap{
Description: "Some headers in the query are missing from the file. Check the file and try again.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidArgument: {
ErrCode: ErrInvalidArgument,
Code: "InvalidArgument",
Description: "The specified argument was invalid.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidColumnIndex: {
ErrCode: ErrInvalidColumnIndex,
Code: "InvalidColumnIndex",

1158
api/handler/acl.go 100644

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,698 @@
package handler
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"net/http"
"testing"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-s3-gw/api"
"github.com/nspcc-dev/neofs-s3-gw/creds/accessbox"
"github.com/stretchr/testify/require"
)
func TestTableToAst(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
key2, err := keys.NewPrivateKey()
require.NoError(t, err)
table := new(eacl.Table)
record := eacl.NewRecord()
record.SetAction(eacl.ActionAllow)
record.SetOperation(eacl.OperationGet)
eacl.AddFormedTarget(record, eacl.RoleOthers)
table.AddRecord(record)
record2 := eacl.NewRecord()
record2.SetAction(eacl.ActionDeny)
record2.SetOperation(eacl.OperationPut)
eacl.AddFormedTarget(record2, eacl.RoleUser, *(*ecdsa.PublicKey)(key.PublicKey()))
eacl.AddFormedTarget(record2, eacl.RoleUser, *(*ecdsa.PublicKey)(key2.PublicKey()))
record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName")
table.AddRecord(record2)
expectedAst := &ast{
Resources: []*astResource{
{
Name: "bucketName",
Operations: []*astOperation{{
Role: eacl.RoleOthers,
Op: eacl.OperationGet,
Action: eacl.ActionAllow,
}}},
{
Name: "objectName",
Operations: []*astOperation{{
Users: []string{
hex.EncodeToString(key.PublicKey().Bytes()),
hex.EncodeToString(key2.PublicKey().Bytes()),
},
Role: eacl.RoleUser,
Op: eacl.OperationPut,
Action: eacl.ActionDeny,
}}},
},
}
actualAst := tableToAst(table, expectedAst.Resources[0].Name)
if actualAst.Resources[0].Name == expectedAst.Resources[0].Name {
require.Equal(t, expectedAst, actualAst)
} else {
require.Equal(t, len(expectedAst.Resources), len(actualAst.Resources))
require.Equal(t, expectedAst.Resources[0], actualAst.Resources[1])
require.Equal(t, expectedAst.Resources[1], actualAst.Resources[0])
}
}
func TestPolicyToAst(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
policy := &bucketPolicy{
Statement: []statement{
{
Effect: "Allow",
Principal: principal{AWS: allUsersWildcard},
Action: []string{"s3:PutObject"},
Resource: []string{"arn:aws:s3:::bucketName"},
},
{
Effect: "Deny",
Principal: principal{
CanonicalUser: hex.EncodeToString(key.PublicKey().Bytes()),
},
Action: []string{"s3:GetObject"},
Resource: []string{"arn:aws:s3:::bucketName/object"},
}},
}
expectedAst := &ast{
Resources: []*astResource{
{
Name: "bucketName",
Operations: []*astOperation{{
Role: eacl.RoleOthers,
Op: eacl.OperationPut,
Action: eacl.ActionAllow,
}},
},
{
Name: "bucketName/object",
Operations: getReadOps(key, eacl.RoleUser, eacl.ActionDeny),
},
},
}
actualAst, err := policyToAst(policy)
require.NoError(t, err)
if actualAst.Resources[0].Name == expectedAst.Resources[0].Name {
require.Equal(t, expectedAst, actualAst)
} else {
require.Equal(t, len(expectedAst.Resources), len(actualAst.Resources))
require.Equal(t, expectedAst.Resources[0], actualAst.Resources[1])
require.Equal(t, expectedAst.Resources[1], actualAst.Resources[0])
}
}
func getReadOps(key *keys.PrivateKey, role eacl.Role, action eacl.Action) []*astOperation {
var result []*astOperation
for _, op := range readOps {
result = append(result, &astOperation{
Users: []string{hex.EncodeToString(key.PublicKey().Bytes())},
Role: role,
Op: op,
Action: action,
})
}
return result
}
func TestMergeAstUnModified(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
child := &ast{
Resources: []*astResource{
{
Name: "objectName",
Operations: []*astOperation{{
Users: []string{hex.EncodeToString(key.PublicKey().Bytes())},
Role: eacl.RoleUser,
Op: eacl.OperationPut,
Action: eacl.ActionDeny,
}},
},
},
}
parent := &ast{
Resources: []*astResource{
{
Name: "bucket",
Operations: []*astOperation{{
Role: eacl.RoleOthers,
Op: eacl.OperationGet,
Action: eacl.ActionAllow,
}},
},
child.Resources[0],
},
}
result, updated := mergeAst(parent, child)
require.False(t, updated)
require.Equal(t, parent, result)
}
func TestMergeAstModified(t *testing.T) {
child := &ast{
Resources: []*astResource{
{
Name: "objectName",
Operations: []*astOperation{{
Role: eacl.RoleOthers,
Op: eacl.OperationPut,
Action: eacl.ActionDeny,
}, {
Users: []string{"user2"},
Role: eacl.RoleUser,
Op: eacl.OperationGet,
Action: eacl.ActionDeny,
}},
},
},
}
parent := &ast{
Resources: []*astResource{
{
Name: "objectName",
Operations: []*astOperation{{
Users: []string{"user1"},
Role: eacl.RoleUser,
Op: eacl.OperationGet,
Action: eacl.ActionDeny,
}},
},
},
}
expected := &ast{
Resources: []*astResource{
{
Name: "objectName",
Operations: []*astOperation{
child.Resources[0].Operations[0],
{
Users: []string{"user1", "user2"},
Role: eacl.RoleUser,
Op: eacl.OperationGet,
Action: eacl.ActionDeny,
},
},
},
},
}
actual, updated := mergeAst(parent, child)
require.True(t, updated)
require.Equal(t, expected, actual)
}
func TestMergeAstModifiedConflict(t *testing.T) {
child := &ast{
Resources: []*astResource{
{
Name: "objectName",
Operations: []*astOperation{{
Users: []string{"user1"},
Role: eacl.RoleUser,
Op: eacl.OperationPut,
Action: eacl.ActionDeny,
}, {
Users: []string{"user3"},
Role: eacl.RoleUser,
Op: eacl.OperationGet,
Action: eacl.ActionAllow,
}},
},
},
}
parent := &ast{
Resources: []*astResource{
{
Name: "objectName",
Operations: []*astOperation{{
Users: []string{"user1"},
Role: eacl.RoleUser,
Op: eacl.OperationPut,
Action: eacl.ActionAllow,
}, {
Users: []string{"user2"},
Role: eacl.RoleUser,
Op: eacl.OperationPut,
Action: eacl.ActionDeny,
}, {
Users: []string{"user3"},
Role: eacl.RoleUser,
Op: eacl.OperationGet,
Action: eacl.ActionDeny,
}},
},
},
}
expected := &ast{
Resources: []*astResource{
{
Name: "objectName",
Operations: []*astOperation{
{
Users: []string{"user2", "user1"},
Role: eacl.RoleUser,
Op: eacl.OperationPut,
Action: eacl.ActionDeny,
}, {
Users: []string{"user3"},
Role: eacl.RoleUser,
Op: eacl.OperationGet,
Action: eacl.ActionAllow,
},
},
},
},
}
actual, updated := mergeAst(parent, child)
require.True(t, updated)
require.Equal(t, expected, actual)
}
func TestAstToTable(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
ast := &ast{
Resources: []*astResource{
{
Name: "bucketName",
Operations: []*astOperation{{
Users: []string{hex.EncodeToString(key.PublicKey().Bytes())},
Role: eacl.RoleUser,
Op: eacl.OperationPut,
Action: eacl.ActionAllow,
}},
},
{
Name: "bucketName/objectName",
Operations: []*astOperation{{
Role: eacl.RoleOthers,
Op: eacl.OperationGet,
Action: eacl.ActionDeny,
}},
},
},
}
expectedTable := eacl.NewTable()
record := eacl.NewRecord()
record.SetAction(eacl.ActionAllow)
record.SetOperation(eacl.OperationPut)
eacl.AddFormedTarget(record, eacl.RoleUser, *(*ecdsa.PublicKey)(key.PublicKey()))
expectedTable.AddRecord(record)
record2 := eacl.NewRecord()
record2.SetAction(eacl.ActionDeny)
record2.SetOperation(eacl.OperationGet)
eacl.AddFormedTarget(record2, eacl.RoleOthers)
record2.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFileName, "objectName")
expectedTable.AddRecord(record2)
actualTable, err := astToTable(ast, "bucketName")
require.NoError(t, err)
require.Equal(t, expectedTable, actualTable)
}
func TestRemoveUsers(t *testing.T) {
resource := &astResource{
Name: "name",
Operations: []*astOperation{{
Users: []string{"user1", "user3"},
Role: eacl.RoleUser,
Op: eacl.OperationPut,
Action: eacl.ActionAllow,
}},
}
op := &astOperation{
Role: eacl.RoleUser,
Op: eacl.OperationPut,
Action: eacl.ActionAllow,
}
removeUsers(resource, op, []string{"user1", "user2"})
require.Equal(t, len(resource.Operations), 1)
require.Equal(t, resource.Name, resource.Name)
require.Equal(t, resource.Operations[0].Users, []string{"user3"})
}
func TestBucketAclToPolicy(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
key2, err := keys.NewPrivateKey()
require.NoError(t, err)
id := hex.EncodeToString(key.PublicKey().Bytes())
id2 := hex.EncodeToString(key2.PublicKey().Bytes())
acl := &AccessControlPolicy{
Owner: Owner{
ID: id,
DisplayName: "user1",
},
AccessControlList: []*Grant{{
Grantee: &Grantee{
URI: allUsersGroup,
Type: acpGroup,
},
Permission: aclRead,
}, {
Grantee: &Grantee{
ID: id2,
Type: acpCanonicalUser,
},
Permission: aclWrite,
}},
Resource: "bucketName",
IsBucket: true,
}
expectedPolicy := &bucketPolicy{
Statement: []statement{
{
Effect: "Allow",
Principal: principal{
CanonicalUser: id,
},
Action: []string{"s3:ListBucket", "s3:ListBucketVersions", "s3:ListBucketMultipartUploads", "s3:PutObject", "s3:DeleteObject"},
Resource: []string{arnAwsPrefix + acl.Resource},
}, {
Effect: "Allow",
Principal: principal{AWS: allUsersWildcard},
Action: []string{"s3:ListBucket", "s3:ListBucketVersions", "s3:ListBucketMultipartUploads"},
Resource: []string{arnAwsPrefix + acl.Resource},
}, {
Effect: "Allow",
Principal: principal{
CanonicalUser: id2,
},
Action: []string{"s3:PutObject", "s3:DeleteObject"},
Resource: []string{arnAwsPrefix + acl.Resource},
},
},
}
actualPolicy, err := aclToPolicy(acl)
require.NoError(t, err)
require.Equal(t, expectedPolicy, actualPolicy)
}
func TestObjectAclToPolicy(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
key2, err := keys.NewPrivateKey()
require.NoError(t, err)
id := hex.EncodeToString(key.PublicKey().Bytes())
id2 := hex.EncodeToString(key2.PublicKey().Bytes())
acl := &AccessControlPolicy{
Owner: Owner{
ID: id,
DisplayName: "user1",
},
AccessControlList: []*Grant{{
Grantee: &Grantee{
ID: id,
Type: acpCanonicalUser,
},
Permission: aclFullControl,
}, {
Grantee: &Grantee{
ID: id2,
Type: acpCanonicalUser,
},
Permission: aclFullControl,
}, {
Grantee: &Grantee{
URI: allUsersGroup,
Type: acpGroup,
},
Permission: aclRead,
}},
Resource: "bucketName/object",
IsBucket: false,
}
expectedPolicy := &bucketPolicy{
Statement: []statement{
{
Effect: "Allow",
Principal: principal{
CanonicalUser: id,
},
Action: []string{"s3:GetObject", "s3:GetObjectVersion"},
Resource: []string{arnAwsPrefix + acl.Resource},
},
{
Effect: "Allow",
Principal: principal{
CanonicalUser: id2,
},
Action: []string{"s3:GetObject", "s3:GetObjectVersion"},
Resource: []string{arnAwsPrefix + acl.Resource},
}, {
Effect: "Allow",
Principal: principal{AWS: allUsersWildcard},
Action: []string{"s3:GetObject", "s3:GetObjectVersion"},
Resource: []string{arnAwsPrefix + acl.Resource},
},
},
}
actualPolicy, err := aclToPolicy(acl)
require.NoError(t, err)
require.Equal(t, expectedPolicy, actualPolicy)
}
func TestParseCannedACLHeaders(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
id := hex.EncodeToString(key.PublicKey().Bytes())
address := key.PublicKey().Address()
req := &http.Request{
Header: map[string][]string{
api.AmzACL: {basicACLReadOnly},
},
}
box := &accessbox.Box{
Gate: &accessbox.GateData{
GateKey: key.PublicKey(),
},
}
ctx := context.Background()
ctx = context.WithValue(ctx, api.BoxData, box)
expectedACL := &AccessControlPolicy{
Owner: Owner{
ID: id,
DisplayName: address,
},
AccessControlList: []*Grant{{
Grantee: &Grantee{
ID: id,
DisplayName: address,
Type: acpCanonicalUser,
},
Permission: aclFullControl,
}, {
Grantee: &Grantee{
URI: allUsersGroup,
Type: acpGroup,
},
Permission: aclRead,
}},
}
actualACL, err := parseACLHeaders(req.WithContext(ctx))
require.NoError(t, err)
require.Equal(t, expectedACL, actualACL)
}
func TestParseACLHeaders(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
id := hex.EncodeToString(key.PublicKey().Bytes())
address := key.PublicKey().Address()
req := &http.Request{
Header: map[string][]string{
api.AmzGrantFullControl: {"id=\"user1\""},
api.AmzGrantRead: {"uri=\"" + allUsersGroup + "\", id=\"user2\""},
api.AmzGrantWrite: {"id=\"user2\", id=\"user3\""},
},
}
box := &accessbox.Box{
Gate: &accessbox.GateData{
GateKey: key.PublicKey(),
},
}
ctx := context.Background()
ctx = context.WithValue(ctx, api.BoxData, box)
expectedACL := &AccessControlPolicy{
Owner: Owner{
ID: id,
DisplayName: address,
},
AccessControlList: []*Grant{{
Grantee: &Grantee{
ID: id,
DisplayName: address,
Type: acpCanonicalUser,
},
Permission: aclFullControl,
}, {
Grantee: &Grantee{
ID: "user1",
Type: acpCanonicalUser,
},
Permission: aclFullControl,
}, {
Grantee: &Grantee{
URI: allUsersGroup,
Type: acpGroup,
},
Permission: aclRead,
}, {
Grantee: &Grantee{
ID: "user2",
Type: acpCanonicalUser,
},
Permission: aclRead,
}, {
Grantee: &Grantee{
ID: "user2",
Type: acpCanonicalUser,
},
Permission: aclWrite,
}, {
Grantee: &Grantee{
ID: "user3",
Type: acpCanonicalUser,
},
Permission: aclWrite,
}},
}
actualACL, err := parseACLHeaders(req.WithContext(ctx))
require.NoError(t, err)
require.Equal(t, expectedACL, actualACL)
}
func TestAddGranteeError(t *testing.T) {
headers := map[string][]string{
api.AmzGrantFullControl: {"i=\"user1\""},
api.AmzGrantRead: {"uri, id=\"user2\""},
api.AmzGrantWrite: {"emailAddress=\"user2\""},
"unknown header": {"something"},
}
expectedList := []*Grant{{
Permission: "predefined",
}}
actualList, err := addGrantees(expectedList, headers, "unknown header1")
require.NoError(t, err)
require.Equal(t, expectedList, actualList)
actualList, err = addGrantees(expectedList, headers, "unknown header")
require.Error(t, err)
require.Nil(t, actualList)
actualList, err = addGrantees(expectedList, headers, api.AmzGrantFullControl)
require.Error(t, err)
require.Nil(t, actualList)
actualList, err = addGrantees(expectedList, headers, api.AmzGrantRead)
require.Error(t, err)
require.Nil(t, actualList)
actualList, err = addGrantees(expectedList, headers, api.AmzGrantWrite)
require.Error(t, err)
require.Nil(t, actualList)
}
func TestBucketAclToTable(t *testing.T) {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
key2, err := keys.NewPrivateKey()
require.NoError(t, err)
id := hex.EncodeToString(key.PublicKey().Bytes())
id2 := hex.EncodeToString(key2.PublicKey().Bytes())
acl := &AccessControlPolicy{
Owner: Owner{
ID: id,
DisplayName: "user1",
},
AccessControlList: []*Grant{{
Grantee: &Grantee{
URI: allUsersGroup,
Type: acpGroup,
},
Permission: aclRead,
}, {
Grantee: &Grantee{
ID: id2,
Type: acpCanonicalUser,
},
Permission: aclWrite,
}},
Resource: "bucketName",
IsBucket: true,
}
expectedTable := new(eacl.Table)
for _, op := range readOps {
expectedTable.AddRecord(getOthersRecord(op, eacl.ActionAllow))
}
for _, op := range writeOps {
expectedTable.AddRecord(getAllowRecord(op, key2.PublicKey()))
}
for _, op := range fullOps {
expectedTable.AddRecord(getAllowRecord(op, key.PublicKey()))
}
for _, op := range fullOps {
expectedTable.AddRecord(getOthersRecord(op, eacl.ActionDeny))
}
actualTable, err := bucketACLToTable(acl)
require.NoError(t, err)
require.Equal(t, expectedTable.Records(), actualTable.Records())
}

View File

@ -2,27 +2,24 @@ package handler
import (
"encoding/xml"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"github.com/nspcc-dev/neofs-s3-gw/api/errors"
"github.com/nspcc-dev/neofs-api-go/pkg/acl"
"github.com/nspcc-dev/neofs-node/pkg/policy"
"github.com/nspcc-dev/neofs-s3-gw/api"
"github.com/nspcc-dev/neofs-s3-gw/api/errors"
"github.com/nspcc-dev/neofs-s3-gw/api/layer"
"go.uber.org/zap"
)
// keywords of predefined basic ACL values.
const (
basicACLPrivate = "private"
basicACLReadOnly = "public-read"
basicACLPublic = "public-read-write"
defaultPolicy = "REP 3"
basicACLPrivate = "private"
basicACLReadOnly = "public-read"
basicACLPublic = "public-read-write"
cannedACLAuthRead = "authenticated-read"
defaultPolicy = "REP 3"
publicBasicRule = 0x0FFFFFFF
)
@ -39,6 +36,50 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
reqInfo = api.GetReqInfo(r.Context())
)
objectACL, err := parseACLHeaders(r)
if err != nil {
h.logAndSendError(w, "could not parse object acl", reqInfo, err)
return
}
objectACL.Resource = reqInfo.BucketName + "/" + reqInfo.ObjectName
bktPolicy, err := aclToPolicy(objectACL)
if err != nil {
h.logAndSendError(w, "could not translate object acl to bucket policy", reqInfo, err)
return
}
astChild, err := policyToAst(bktPolicy)
if err != nil {
h.logAndSendError(w, "could not translate policy to ast", reqInfo, err)
return
}
bacl, err := h.obj.GetBucketACL(r.Context(), reqInfo.BucketName)
if err != nil {
h.logAndSendError(w, "could not get bucket eacl", reqInfo, err)
return
}
if err = checkOwner(bacl.Info, r.Header.Get(api.AmzExpectedBucketOwner)); err != nil {
h.logAndSendError(w, "expected owner doesn't match", reqInfo, err)
return
}
parentAst := tableToAst(bacl.EACL, reqInfo.BucketName)
for _, resource := range parentAst.Resources {
if resource.Name == bacl.Info.CID.String() {
resource.Name = reqInfo.BucketName
}
}
resAst, updated := mergeAst(parentAst, astChild)
table, err := astToTable(resAst, reqInfo.BucketName)
if err != nil {
h.logAndSendError(w, "could not translate ast to table", reqInfo, err)
return
}
metadata := parseMetadata(r)
if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 {
metadata[api.ContentType] = contentType
@ -57,6 +98,18 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
return
}
if updated {
p := &layer.PutBucketACLParams{
Name: reqInfo.BucketName,
EACL: table,
}
if err = h.obj.PutBucketACL(r.Context(), p); err != nil {
h.logAndSendError(w, "could not put bucket acl", reqInfo, err)
return
}
}
w.Header().Set(api.ETag, info.HashSum)
api.WriteSuccessResponseHeadersOnly(w)
}
@ -74,24 +127,25 @@ func parseMetadata(r *http.Request) map[string]string {
func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) {
var (
err error
reqInfo = api.GetReqInfo(r.Context())
p = layer.CreateBucketParams{Name: reqInfo.BucketName}
p = layer.CreateBucketParams{Name: reqInfo.BucketName, ACL: publicBasicRule}
)
if err = checkBucketName(reqInfo.BucketName); err != nil {
if err := checkBucketName(reqInfo.BucketName); err != nil {
h.logAndSendError(w, "invalid bucket name", reqInfo, err)
return
}
if val, ok := r.Header["X-Amz-Acl"]; ok {
p.ACL, err = parseBasicACL(val[0])
} else {
p.ACL = publicBasicRule
}
bktACL, err := parseACLHeaders(r)
if err != nil {
h.logAndSendError(w, "could not parse basic ACL", reqInfo, err)
h.logAndSendError(w, "could not parse bucket acl", reqInfo, err)
return
}
bktACL.IsBucket = true
p.EACL, err = bucketACLToTable(bktACL)
if err != nil {
h.logAndSendError(w, "could translate bucket acl to eacl", reqInfo, err)
return
}
@ -180,23 +234,3 @@ func parseLocationConstraint(r *http.Request) (*createBucketParams, error) {
}
return params, nil
}
func parseBasicACL(basicACL string) (uint32, error) {
switch basicACL {
case basicACLPublic:
return acl.PublicBasicRule, nil
case basicACLPrivate:
return acl.PrivateBasicRule, nil
case basicACLReadOnly:
return acl.ReadOnlyBasicRule, nil
default:
basicACL = strings.Trim(strings.ToLower(basicACL), "0x")
value, err := strconv.ParseUint(basicACL, 16, 32)
if err != nil {
return 0, fmt.Errorf("can't parse basic ACL: %s", basicACL)
}
return uint32(value), nil
}
}

View File

@ -52,6 +52,41 @@ type Bucket struct {
CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
}
// AccessControlPolicy contains ACL.
type AccessControlPolicy struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ AccessControlPolicy" json:"-"`
Owner Owner
AccessControlList []*Grant `xml:"AccessControlList>Grant"`
Resource string `xml:"-"`
IsBucket bool `xml:"-"`
}
// Grant is container for Grantee data.
type Grant struct {
Grantee *Grantee
Permission AWSACL
}
// Grantee is info about access rights of some actor.
type Grantee struct {
XMLName xml.Name `xml:"Grantee"`
XMLNS string `xml:"xmlns:xsi,attr"`
ID string `xml:"ID,omitempty"`
DisplayName string `xml:"DisplayName,omitempty"`
EmailAddress string `xml:"EmailAddress,omitempty"`
URI string `xml:"URI,omitempty"`
Type GranteeType `xml:"xsi:type,attr"`
}
// NewGrantee creates new grantee using workaround
// https://github.com/golang/go/issues/9519#issuecomment-252196382
func NewGrantee(t GranteeType) *Grantee {
return &Grantee{
XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
Type: t,
}
}
// Owner - bucket owner/principal.
type Owner struct {
ID string

View File

@ -31,14 +31,6 @@ func (h *handler) AbortMultipartUploadHandler(w http.ResponseWriter, r *http.Req
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
@ -71,10 +63,6 @@ func (h *handler) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Reque
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
@ -83,14 +71,6 @@ func (h *handler) GetBucketEncryptionHandler(w http.ResponseWriter, r *http.Requ
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
@ -151,10 +131,6 @@ func (h *handler) PutBucketEncryptionHandler(w http.ResponseWriter, r *http.Requ
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}
func (h *handler) PutBucketObjectLockConfigHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
}

View File

@ -34,4 +34,9 @@ const (
AmzCopyIfUnmodifiedSince = "X-Amz-Copy-Source-If-Unmodified-Since"
AmzCopyIfMatch = "X-Amz-Copy-Source-If-Match"
AmzCopyIfNoneMatch = "X-Amz-Copy-Source-If-None-Match"
AmzACL = "X-Amz-Acl"
AmzGrantFullControl = "X-Amz-Grant-Full-Control"
AmzGrantRead = "X-Amz-Grant-Read"
AmzGrantWrite = "X-Amz-Grant-Write"
AmzExpectedBucketOwner = "X-Amz-Expected-Bucket-Owner"
)

View File

@ -3,13 +3,11 @@ package layer
import (
"bytes"
"context"
"crypto/ecdsa"
"fmt"
"strconv"
"strings"
"time"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
cid "github.com/nspcc-dev/neofs-api-go/pkg/container/id"
@ -23,10 +21,16 @@ import (
type (
// BucketInfo stores basic bucket data.
BucketInfo struct {
Name string
CID *cid.ID
Owner *owner.ID
Created time.Time
Name string
CID *cid.ID
Owner *owner.ID
Created time.Time
BasicACL uint32
}
// BucketACL extends BucketInfo by eacl.Table.
BucketACL struct {
Info *BucketInfo
EACL *eacl.Table
}
)
@ -56,6 +60,7 @@ func (n *layer) containerInfo(ctx context.Context, cid *cid.ID) (*BucketInfo, er
}
info.Owner = res.OwnerID()
info.BasicACL = res.BasicACL()
for _, attr := range res.Attributes() {
switch key, val := attr.Key(), attr.Value(); key {
@ -131,19 +136,15 @@ func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*ci
return nil, err
}
if err := n.setContainerEACL(ctx, cid, p.BoxData.Gate.GateKey); err != nil {
if err := n.setContainerEACLTable(ctx, cid, p.EACL); err != nil {
return nil, err
}
return cid, nil
}
func (n *layer) setContainerEACL(ctx context.Context, cid *cid.ID, gateKey *keys.PublicKey) error {
if gateKey == nil {
return fmt.Errorf("gate key must not be nil")
}
table := formDefaultTable(cid, *(*ecdsa.PublicKey)(gateKey))
func (n *layer) setContainerEACLTable(ctx context.Context, cid *cid.ID, table *eacl.Table) error {
table.SetCID(cid)
if err := n.pool.SetEACL(ctx, table, n.SessionOpt(ctx)); err != nil {
return err
}
@ -155,25 +156,12 @@ func (n *layer) setContainerEACL(ctx context.Context, cid *cid.ID, gateKey *keys
return nil
}
func formDefaultTable(cid *cid.ID, gateKey ecdsa.PublicKey) *eacl.Table {
table := eacl.NewTable()
table.SetCID(cid)
for op := eacl.OperationGet; op <= eacl.OperationRangeHash; op++ {
record := eacl.NewRecord()
record.SetOperation(op)
record.SetAction(eacl.ActionAllow)
eacl.AddFormedTarget(record, eacl.RoleUser, gateKey)
table.AddRecord(record)
record2 := eacl.NewRecord()
record2.SetOperation(op)
record2.SetAction(eacl.ActionDeny)
eacl.AddFormedTarget(record2, eacl.RoleOthers)
table.AddRecord(record2)
func (n *layer) GetContainerEACL(ctx context.Context, cid *cid.ID) (*eacl.Table, error) {
signedEacl, err := n.pool.GetEACL(ctx, cid)
if err != nil {
return nil, err
}
return table
return signedEacl.EACL(), nil
}
type waitParams struct {

View File

@ -9,6 +9,7 @@ import (
"sort"
"time"
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/client"
cid "github.com/nspcc-dev/neofs-api-go/pkg/container/id"
"github.com/nspcc-dev/neofs-api-go/pkg/netmap"
@ -84,8 +85,14 @@ type (
Name string
ACL uint32
Policy *netmap.PlacementPolicy
EACL *eacl.Table
BoxData *accessbox.Box
}
// PutBucketACLParams stores put bucket acl request parameters.
PutBucketACLParams struct {
Name string
EACL *eacl.Table
}
// DeleteBucketParams stores delete bucket request parameters.
DeleteBucketParams struct {
Name string
@ -112,6 +119,8 @@ type (
ListBuckets(ctx context.Context) ([]*BucketInfo, error)
GetBucketInfo(ctx context.Context, name string) (*BucketInfo, error)
GetBucketACL(ctx context.Context, name string) (*BucketACL, error)
PutBucketACL(ctx context.Context, p *PutBucketACLParams) error
CreateBucket(ctx context.Context, p *CreateBucketParams) (*cid.ID, error)
DeleteBucket(ctx context.Context, p *DeleteBucketParams) error
@ -204,6 +213,34 @@ func (n *layer) GetBucketInfo(ctx context.Context, name string) (*BucketInfo, er
return n.containerInfo(ctx, containerID)
}
// GetBucketACL returns bucket acl info by name.
func (n *layer) GetBucketACL(ctx context.Context, name string) (*BucketACL, error) {
inf, err := n.GetBucketInfo(ctx, name)
if err != nil {
return nil, err
}
eacl, err := n.GetContainerEACL(ctx, inf.CID)
if err != nil {
return nil, err
}
return &BucketACL{
Info: inf,
EACL: eacl,
}, nil
}
// PutBucketACL put bucket acl by name.
func (n *layer) PutBucketACL(ctx context.Context, param *PutBucketACLParams) error {
inf, err := n.GetBucketInfo(ctx, param.Name)
if err != nil {
return err
}
return n.setContainerEACLTable(ctx, inf.CID, param.EACL)
}
// ListBuckets returns all user containers. Name of the bucket is a container
// id. Timestamp is omitted since it is not saved in neofs container.
func (n *layer) ListBuckets(ctx context.Context) ([]*BucketInfo, error) {