[#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:
parent
b1c6629b10
commit
efe11c271f
10 changed files with 2046 additions and 100 deletions
20
README.md
20
README.md
|
@ -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
|
||||||
|
|
|
@ -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
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
698
api/handler/acl_test.go
Normal 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())
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue