[#49] Add basic ACL translation

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

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
This commit is contained in:
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: Reference:
* [AWS S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/s3-api.pdf) * [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 ### Object
| Method | Status | | Method | Status |
@ -469,8 +477,8 @@ Reference:
| Method | Status | | Method | Status |
| ------------------------- | ----------------------- | | ------------------------- | ----------------------- |
| GetObjectAcl | Unsupported | | GetObjectAcl | Supported |
| PutObjectAcl | Unsupported | | PutObjectAcl | Supported |
#### Locking #### Locking
@ -540,8 +548,8 @@ See also `GetObject` and other method parameters.
| Method | Status | | Method | Status |
| ------------------------- | ----------------------- | | ------------------------- | ----------------------- |
| GetBucketAcl | Unsupported | | GetBucketAcl | Supported |
| PutBucketAcl | Unsupported | | PutBucketAcl | Supported |
#### Analytics #### Analytics
@ -630,11 +638,11 @@ See also `GetObject` and other method parameters.
| DeleteBucketPolicy | Unsupported | | DeleteBucketPolicy | Unsupported |
| DeleteBucketReplication | Unsupported | | DeleteBucketReplication | Unsupported |
| DeletePublicAccessBlock | Unsupported | | DeletePublicAccessBlock | Unsupported |
| GetBucketPolicy | Unsupported | | GetBucketPolicy | Supported |
| GetBucketPolicyStatus | Unsupported | | GetBucketPolicyStatus | Unsupported |
| GetBucketReplication | Unsupported | | GetBucketReplication | Unsupported |
| PostPolicyBucket | Unsupported, non-standard? | | PostPolicyBucket | Unsupported, non-standard? |
| PutBucketPolicy | Unsupported | | PutBucketPolicy | Supported |
| PutBucketReplication | Unsupported | | PutBucketReplication | Unsupported |
#### Request payment #### Request payment

View file

@ -298,6 +298,7 @@ const (
ErrEvaluatorInvalidTimestampFormatPatternSymbol ErrEvaluatorInvalidTimestampFormatPatternSymbol
ErrEvaluatorBindingDoesNotExist ErrEvaluatorBindingDoesNotExist
ErrMissingHeaders ErrMissingHeaders
ErrInvalidArgument
ErrInvalidColumnIndex ErrInvalidColumnIndex
ErrAdminConfigNotificationTargetsFailed 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.", Description: "Some headers in the query are missing from the file. Check the file and try again.",
HTTPStatusCode: http.StatusBadRequest, HTTPStatusCode: http.StatusBadRequest,
}, },
ErrInvalidArgument: {
ErrCode: ErrInvalidArgument,
Code: "InvalidArgument",
Description: "The specified argument was invalid.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidColumnIndex: { ErrInvalidColumnIndex: {
ErrCode: ErrInvalidColumnIndex, ErrCode: ErrInvalidColumnIndex,
Code: "InvalidColumnIndex", Code: "InvalidColumnIndex",

1158
api/handler/acl.go Normal file

File diff suppressed because it is too large Load diff

698
api/handler/acl_test.go Normal file
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 ( import (
"encoding/xml" "encoding/xml"
"fmt"
"net" "net"
"net/http" "net/http"
"strconv"
"strings" "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-node/pkg/policy"
"github.com/nspcc-dev/neofs-s3-gw/api" "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" "github.com/nspcc-dev/neofs-s3-gw/api/layer"
"go.uber.org/zap" "go.uber.org/zap"
) )
// keywords of predefined basic ACL values. // keywords of predefined basic ACL values.
const ( const (
basicACLPrivate = "private" basicACLPrivate = "private"
basicACLReadOnly = "public-read" basicACLReadOnly = "public-read"
basicACLPublic = "public-read-write" basicACLPublic = "public-read-write"
defaultPolicy = "REP 3" cannedACLAuthRead = "authenticated-read"
defaultPolicy = "REP 3"
publicBasicRule = 0x0FFFFFFF publicBasicRule = 0x0FFFFFFF
) )
@ -39,6 +36,50 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
reqInfo = api.GetReqInfo(r.Context()) 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) 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
@ -57,6 +98,18 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
return 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) w.Header().Set(api.ETag, info.HashSum)
api.WriteSuccessResponseHeadersOnly(w) 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) { func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) {
var ( var (
err error
reqInfo = api.GetReqInfo(r.Context()) 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) h.logAndSendError(w, "invalid bucket name", reqInfo, err)
return return
} }
if val, ok := r.Header["X-Amz-Acl"]; ok { bktACL, err := parseACLHeaders(r)
p.ACL, err = parseBasicACL(val[0])
} else {
p.ACL = publicBasicRule
}
if err != nil { 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 return
} }
@ -180,23 +234,3 @@ func parseLocationConstraint(r *http.Request) (*createBucketParams, error) {
} }
return params, nil 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" 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. // Owner - bucket owner/principal.
type Owner struct { type Owner struct {
ID string 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)) 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) { func (h *handler) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented)) 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)) 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) { func (h *handler) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented)) 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)) 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) { func (h *handler) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented)) 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)) 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) { func (h *handler) PutBucketObjectLockConfigHandler(w http.ResponseWriter, r *http.Request) {
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented)) 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" AmzCopyIfUnmodifiedSince = "X-Amz-Copy-Source-If-Unmodified-Since"
AmzCopyIfMatch = "X-Amz-Copy-Source-If-Match" AmzCopyIfMatch = "X-Amz-Copy-Source-If-Match"
AmzCopyIfNoneMatch = "X-Amz-Copy-Source-If-None-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 ( import (
"bytes" "bytes"
"context" "context"
"crypto/ecdsa"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time" "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/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/container" "github.com/nspcc-dev/neofs-api-go/pkg/container"
cid "github.com/nspcc-dev/neofs-api-go/pkg/container/id" cid "github.com/nspcc-dev/neofs-api-go/pkg/container/id"
@ -23,10 +21,16 @@ import (
type ( type (
// BucketInfo stores basic bucket data. // BucketInfo stores basic bucket data.
BucketInfo struct { BucketInfo struct {
Name string Name string
CID *cid.ID CID *cid.ID
Owner *owner.ID Owner *owner.ID
Created time.Time 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.Owner = res.OwnerID()
info.BasicACL = res.BasicACL()
for _, attr := range res.Attributes() { for _, attr := range res.Attributes() {
switch key, val := attr.Key(), attr.Value(); key { 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 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 nil, err
} }
return cid, nil return cid, nil
} }
func (n *layer) setContainerEACL(ctx context.Context, cid *cid.ID, gateKey *keys.PublicKey) error { func (n *layer) setContainerEACLTable(ctx context.Context, cid *cid.ID, table *eacl.Table) error {
if gateKey == nil { table.SetCID(cid)
return fmt.Errorf("gate key must not be nil")
}
table := formDefaultTable(cid, *(*ecdsa.PublicKey)(gateKey))
if err := n.pool.SetEACL(ctx, table, n.SessionOpt(ctx)); err != nil { if err := n.pool.SetEACL(ctx, table, n.SessionOpt(ctx)); err != nil {
return err return err
} }
@ -155,25 +156,12 @@ func (n *layer) setContainerEACL(ctx context.Context, cid *cid.ID, gateKey *keys
return nil return nil
} }
func formDefaultTable(cid *cid.ID, gateKey ecdsa.PublicKey) *eacl.Table { func (n *layer) GetContainerEACL(ctx context.Context, cid *cid.ID) (*eacl.Table, error) {
table := eacl.NewTable() signedEacl, err := n.pool.GetEACL(ctx, cid)
table.SetCID(cid) if err != nil {
return nil, err
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)
} }
return signedEacl.EACL(), nil
return table
} }
type waitParams struct { type waitParams struct {

View file

@ -9,6 +9,7 @@ import (
"sort" "sort"
"time" "time"
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/client" "github.com/nspcc-dev/neofs-api-go/pkg/client"
cid "github.com/nspcc-dev/neofs-api-go/pkg/container/id" cid "github.com/nspcc-dev/neofs-api-go/pkg/container/id"
"github.com/nspcc-dev/neofs-api-go/pkg/netmap" "github.com/nspcc-dev/neofs-api-go/pkg/netmap"
@ -84,8 +85,14 @@ type (
Name string Name string
ACL uint32 ACL uint32
Policy *netmap.PlacementPolicy Policy *netmap.PlacementPolicy
EACL *eacl.Table
BoxData *accessbox.Box BoxData *accessbox.Box
} }
// PutBucketACLParams stores put bucket acl request parameters.
PutBucketACLParams struct {
Name string
EACL *eacl.Table
}
// DeleteBucketParams stores delete bucket request parameters. // DeleteBucketParams stores delete bucket request parameters.
DeleteBucketParams struct { DeleteBucketParams struct {
Name string Name string
@ -112,6 +119,8 @@ type (
ListBuckets(ctx context.Context) ([]*BucketInfo, error) ListBuckets(ctx context.Context) ([]*BucketInfo, error)
GetBucketInfo(ctx context.Context, name string) (*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) CreateBucket(ctx context.Context, p *CreateBucketParams) (*cid.ID, error)
DeleteBucket(ctx context.Context, p *DeleteBucketParams) 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) 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 // ListBuckets returns all user containers. Name of the bucket is a container
// id. Timestamp is omitted since it is not saved in neofs container. // id. Timestamp is omitted since it is not saved in neofs container.
func (n *layer) ListBuckets(ctx context.Context) ([]*BucketInfo, error) { func (n *layer) ListBuckets(ctx context.Context) ([]*BucketInfo, error) {