forked from TrueCloudLab/frostfs-s3-gw
1738 lines
46 KiB
Go
1738 lines
46 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
stderrors "errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
v2acl "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
|
engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam"
|
|
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var (
|
|
writeOps = []eacl.Operation{eacl.OperationPut, eacl.OperationDelete}
|
|
readOps = []eacl.Operation{eacl.OperationGet, eacl.OperationHead,
|
|
eacl.OperationSearch, eacl.OperationRange, eacl.OperationRangeHash}
|
|
fullOps = []eacl.Operation{eacl.OperationGet, eacl.OperationHead, eacl.OperationPut,
|
|
eacl.OperationDelete, eacl.OperationSearch, eacl.OperationRange, eacl.OperationRangeHash}
|
|
)
|
|
|
|
var actionToOpMap = map[string][]eacl.Operation{
|
|
s3DeleteObject: {eacl.OperationDelete},
|
|
s3GetObject: readOps,
|
|
s3PutObject: {eacl.OperationPut},
|
|
s3ListBucket: readOps,
|
|
}
|
|
|
|
const (
|
|
arnAwsPrefix = "arn:aws:s3:::"
|
|
allUsersWildcard = "*"
|
|
allUsersGroup = "http://acs.amazonaws.com/groups/global/AllUsers"
|
|
|
|
s3DeleteObject = "s3:DeleteObject"
|
|
s3GetObject = "s3:GetObject"
|
|
s3PutObject = "s3:PutObject"
|
|
s3ListBucket = "s3:ListBucket"
|
|
s3ListBucketVersions = "s3:ListBucketVersions"
|
|
s3ListBucketMultipartUploads = "s3:ListBucketMultipartUploads"
|
|
s3GetObjectVersion = "s3:GetObjectVersion"
|
|
)
|
|
|
|
// AWSACL is aws permission constants.
|
|
type AWSACL string
|
|
|
|
const (
|
|
aclFullControl AWSACL = "FULL_CONTROL"
|
|
aclWrite AWSACL = "WRITE"
|
|
aclRead AWSACL = "READ"
|
|
)
|
|
|
|
// GranteeType is aws grantee permission type constants.
|
|
type GranteeType string
|
|
|
|
const (
|
|
acpCanonicalUser GranteeType = "CanonicalUser"
|
|
acpAmazonCustomerByEmail GranteeType = "AmazonCustomerByEmail"
|
|
acpGroup GranteeType = "Group"
|
|
)
|
|
|
|
type bucketPolicy struct {
|
|
Version string `json:"Version"`
|
|
ID string `json:"Id"`
|
|
Statement []statement `json:"Statement"`
|
|
Bucket string `json:"-"`
|
|
}
|
|
|
|
type statement struct {
|
|
Sid string `json:"Sid"`
|
|
Effect string `json:"Effect"`
|
|
Principal principal `json:"Principal"`
|
|
Action []string `json:"Action"`
|
|
Resource []string `json:"Resource"`
|
|
}
|
|
|
|
type principal struct {
|
|
AWS string `json:"AWS,omitempty"`
|
|
CanonicalUser string `json:"CanonicalUser,omitempty"`
|
|
}
|
|
|
|
type orderedAstResource struct {
|
|
Index int
|
|
Resource *astResource
|
|
}
|
|
|
|
type ast struct {
|
|
Resources []*astResource
|
|
}
|
|
|
|
type astResource struct {
|
|
resourceInfo
|
|
Operations []*astOperation
|
|
}
|
|
|
|
type resourceInfo struct {
|
|
Bucket string
|
|
Object string
|
|
Version string
|
|
}
|
|
|
|
func (r *resourceInfo) Name() string {
|
|
if len(r.Object) == 0 {
|
|
return r.Bucket
|
|
}
|
|
if len(r.Version) == 0 {
|
|
return r.Bucket + "/" + r.Object
|
|
}
|
|
return r.Bucket + "/" + r.Object + ":" + r.Version
|
|
}
|
|
|
|
func (r *resourceInfo) IsBucket() bool {
|
|
return len(r.Object) == 0
|
|
}
|
|
|
|
type astOperation struct {
|
|
Users []string
|
|
Op eacl.Operation
|
|
Action eacl.Action
|
|
}
|
|
|
|
func (a astOperation) IsGroupGrantee() bool {
|
|
return len(a.Users) == 0
|
|
}
|
|
|
|
const (
|
|
serviceRecordResourceKey = "Resource"
|
|
serviceRecordGroupLengthKey = "GroupLength"
|
|
)
|
|
|
|
type ServiceRecord struct {
|
|
Resource string
|
|
GroupRecordsLength int
|
|
}
|
|
|
|
func (s ServiceRecord) ToEACLRecord() *eacl.Record {
|
|
serviceRecord := eacl.NewRecord()
|
|
serviceRecord.SetAction(eacl.ActionAllow)
|
|
serviceRecord.SetOperation(eacl.OperationGet)
|
|
serviceRecord.AddFilter(eacl.HeaderFromService, eacl.MatchUnknown, serviceRecordResourceKey, s.Resource)
|
|
serviceRecord.AddFilter(eacl.HeaderFromService, eacl.MatchUnknown, serviceRecordGroupLengthKey, strconv.Itoa(s.GroupRecordsLength))
|
|
eacl.AddFormedTarget(serviceRecord, eacl.RoleSystem)
|
|
return serviceRecord
|
|
}
|
|
|
|
var (
|
|
errInvalidStatement = stderrors.New("invalid statement")
|
|
errInvalidPrincipal = stderrors.New("invalid principal")
|
|
)
|
|
|
|
func (s *statement) UnmarshalJSON(data []byte) error {
|
|
var statementMap map[string]interface{}
|
|
if err := json.Unmarshal(data, &statementMap); err != nil {
|
|
return err
|
|
}
|
|
|
|
sidField, ok := statementMap["Sid"]
|
|
if ok {
|
|
if s.Sid, ok = sidField.(string); !ok {
|
|
return errInvalidStatement
|
|
}
|
|
}
|
|
|
|
effectField, ok := statementMap["Effect"]
|
|
if ok {
|
|
if s.Effect, ok = effectField.(string); !ok {
|
|
return errInvalidStatement
|
|
}
|
|
}
|
|
|
|
principalField, ok := statementMap["Principal"]
|
|
if ok {
|
|
principalMap, ok := principalField.(map[string]interface{})
|
|
if !ok {
|
|
return errInvalidPrincipal
|
|
}
|
|
|
|
awsField, ok := principalMap["AWS"]
|
|
if ok {
|
|
if s.Principal.AWS, ok = awsField.(string); !ok {
|
|
return fmt.Errorf("%w: 'AWS' field must be string", errInvalidPrincipal)
|
|
}
|
|
}
|
|
|
|
canonicalUserField, ok := principalMap["CanonicalUser"]
|
|
if ok {
|
|
if s.Principal.CanonicalUser, ok = canonicalUserField.(string); !ok {
|
|
return errInvalidPrincipal
|
|
}
|
|
}
|
|
}
|
|
|
|
actionField, ok := statementMap["Action"]
|
|
if ok {
|
|
switch actionField := actionField.(type) {
|
|
case []interface{}:
|
|
s.Action = make([]string, len(actionField))
|
|
for i, action := range actionField {
|
|
if s.Action[i], ok = action.(string); !ok {
|
|
return errInvalidStatement
|
|
}
|
|
}
|
|
case string:
|
|
s.Action = []string{actionField}
|
|
default:
|
|
return errInvalidStatement
|
|
}
|
|
}
|
|
|
|
resourceField, ok := statementMap["Resource"]
|
|
if ok {
|
|
switch resourceField := resourceField.(type) {
|
|
case []interface{}:
|
|
s.Resource = make([]string, len(resourceField))
|
|
for i, action := range resourceField {
|
|
if s.Resource[i], ok = action.(string); !ok {
|
|
return errInvalidStatement
|
|
}
|
|
}
|
|
case string:
|
|
s.Resource = []string{resourceField}
|
|
default:
|
|
return errInvalidStatement
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
reqInfo := middleware.GetReqInfo(ctx)
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
if bktInfo.APEEnabled || len(settings.CannedACL) != 0 {
|
|
if err = middleware.EncodeToResponse(w, h.encodeBucketCannedACL(ctx, bktInfo, settings)); err != nil {
|
|
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
bucketACL, err := h.obj.GetBucketACL(ctx, bktInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not fetch bucket acl", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
if err = middleware.EncodeToResponse(w, h.encodeBucketACL(ctx, bktInfo.Name, bucketACL)); err != nil {
|
|
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *handler) encodeBucketCannedACL(ctx context.Context, bktInfo *data.BucketInfo, settings *data.BucketSettings) *AccessControlPolicy {
|
|
ownerDisplayName := bktInfo.Owner.EncodeToString()
|
|
ownerEncodedID := ownerDisplayName
|
|
|
|
if settings.OwnerKey == nil {
|
|
h.reqLogger(ctx).Warn(logs.BucketOwnerKeyIsMissing, zap.String("owner", bktInfo.Owner.String()))
|
|
} else {
|
|
ownerDisplayName = settings.OwnerKey.Address()
|
|
ownerEncodedID = hex.EncodeToString(settings.OwnerKey.Bytes())
|
|
}
|
|
|
|
res := &AccessControlPolicy{Owner: Owner{
|
|
ID: ownerEncodedID,
|
|
DisplayName: ownerDisplayName,
|
|
}}
|
|
|
|
granteeOwner := NewGrantee(acpCanonicalUser)
|
|
granteeOwner.ID = ownerEncodedID
|
|
granteeOwner.DisplayName = ownerDisplayName
|
|
|
|
res.AccessControlList = []*Grant{{
|
|
Grantee: granteeOwner,
|
|
Permission: aclFullControl,
|
|
}}
|
|
|
|
switch settings.CannedACL {
|
|
case basicACLPublic:
|
|
grantee := NewGrantee(acpGroup)
|
|
grantee.URI = allUsersGroup
|
|
|
|
res.AccessControlList = append(res.AccessControlList, &Grant{
|
|
Grantee: grantee,
|
|
Permission: aclWrite,
|
|
})
|
|
fallthrough
|
|
case basicACLReadOnly:
|
|
grantee := NewGrantee(acpGroup)
|
|
grantee.URI = allUsersGroup
|
|
|
|
res.AccessControlList = append(res.AccessControlList, &Grant{
|
|
Grantee: grantee,
|
|
Permission: aclRead,
|
|
})
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func (h *handler) bearerTokenIssuerKey(ctx context.Context) (*keys.PublicKey, error) {
|
|
box, err := middleware.GetBoxData(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return getTokenIssuerKey(box)
|
|
}
|
|
|
|
func getTokenIssuerKey(box *accessbox.Box) (*keys.PublicKey, error) {
|
|
if box.Gate.BearerToken == nil {
|
|
return nil, stderrors.New("bearer token is missing")
|
|
}
|
|
|
|
key, err := keys.NewPublicKeyFromBytes(box.Gate.BearerToken.SigningKeyBytes(), elliptic.P256())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("public key from bytes: %w", err)
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
|
reqInfo := middleware.GetReqInfo(r.Context())
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
if bktInfo.APEEnabled || len(settings.CannedACL) != 0 {
|
|
h.putBucketACLAPEHandler(w, r, reqInfo, bktInfo, settings)
|
|
return
|
|
}
|
|
|
|
key, err := h.bearerTokenIssuerKey(r.Context())
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get bearer token issuer key", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
token, err := getSessionTokenSetEACL(r.Context())
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get eacl token", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
list := &AccessControlPolicy{}
|
|
if r.ContentLength == 0 {
|
|
list, err = parseACLHeaders(r.Header, key)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not parse bucket acl", reqInfo, err)
|
|
return
|
|
}
|
|
} else if err = h.cfg.NewXMLDecoder(r.Body).Decode(list); err != nil {
|
|
h.logAndSendError(w, "could not parse bucket acl", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
|
|
return
|
|
}
|
|
|
|
resInfo := &resourceInfo{Bucket: reqInfo.BucketName}
|
|
astBucket, err := aclToAst(list, resInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not translate acl to policy", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
if _, err = h.updateBucketACL(r, astBucket, bktInfo, token); err != nil {
|
|
h.logAndSendError(w, "could not update bucket acl", reqInfo, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (h *handler) putBucketACLAPEHandler(w http.ResponseWriter, r *http.Request, reqInfo *middleware.ReqInfo, bktInfo *data.BucketInfo, settings *data.BucketSettings) {
|
|
ctx := r.Context()
|
|
|
|
defer func() {
|
|
if errBody := r.Body.Close(); errBody != nil {
|
|
h.reqLogger(r.Context()).Warn(logs.CouldNotCloseRequestBody, zap.Error(errBody))
|
|
}
|
|
}()
|
|
|
|
written, err := io.Copy(io.Discard, r.Body)
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't read request body", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
if written != 0 || len(r.Header.Get(api.AmzACL)) == 0 {
|
|
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
|
return
|
|
}
|
|
|
|
cannedACL, err := parseCannedACL(r.Header)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not parse canned ACL", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
key, err := h.bearerTokenIssuerKey(ctx)
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get bearer token issuer key", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, key, bktInfo.CID)
|
|
if err = h.ape.SaveACLChains(reqInfo.Namespace, chainRules); err != nil {
|
|
h.logAndSendError(w, "failed to add morph rule chains", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
settings.CannedACL = cannedACL
|
|
|
|
sp := &layer.PutSettingsParams{
|
|
BktInfo: bktInfo,
|
|
Settings: settings,
|
|
}
|
|
|
|
if err = h.obj.PutBucketSettings(ctx, sp); err != nil {
|
|
h.logAndSendError(w, "couldn't save bucket settings", reqInfo, err,
|
|
zap.String("container_id", bktInfo.CID.EncodeToString()))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (h *handler) updateBucketACL(r *http.Request, astChild *ast, bktInfo *data.BucketInfo, sessionToken *session.Container) (bool, error) {
|
|
bucketACL, err := h.obj.GetBucketACL(r.Context(), bktInfo)
|
|
if err != nil {
|
|
return false, fmt.Errorf("could not get bucket eacl: %w", err)
|
|
}
|
|
|
|
parentAst := tableToAst(bucketACL.EACL, bktInfo.Name)
|
|
strCID := bucketACL.Info.CID.EncodeToString()
|
|
|
|
for _, resource := range parentAst.Resources {
|
|
if resource.Bucket == strCID {
|
|
resource.Bucket = bktInfo.Name
|
|
}
|
|
}
|
|
|
|
resAst, updated := mergeAst(parentAst, astChild)
|
|
if !updated {
|
|
return false, nil
|
|
}
|
|
|
|
table, err := astToTable(resAst)
|
|
if err != nil {
|
|
return false, fmt.Errorf("could not translate ast to table: %w", err)
|
|
}
|
|
|
|
p := &layer.PutBucketACLParams{
|
|
BktInfo: bktInfo,
|
|
EACL: table,
|
|
SessionToken: sessionToken,
|
|
}
|
|
|
|
if err = h.obj.PutBucketACL(r.Context(), p); err != nil {
|
|
return false, fmt.Errorf("could not put bucket acl: %w", err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
reqInfo := middleware.GetReqInfo(ctx)
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
apeEnabled := bktInfo.APEEnabled
|
|
|
|
if !apeEnabled {
|
|
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
|
return
|
|
}
|
|
apeEnabled = len(settings.CannedACL) != 0
|
|
}
|
|
|
|
if apeEnabled {
|
|
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
|
return
|
|
}
|
|
|
|
bucketACL, err := h.obj.GetBucketACL(ctx, bktInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not fetch bucket acl", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
prm := &layer.HeadObjectParams{
|
|
BktInfo: bktInfo,
|
|
Object: reqInfo.ObjectName,
|
|
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
|
}
|
|
|
|
objInfo, err := h.obj.GetObjectInfo(ctx, prm)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not object info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
if err = middleware.EncodeToResponse(w, h.encodeObjectACL(ctx, bucketACL, reqInfo.BucketName, objInfo.VersionID())); err != nil {
|
|
h.logAndSendError(w, "failed to encode response", reqInfo, err)
|
|
}
|
|
}
|
|
|
|
func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
reqInfo := middleware.GetReqInfo(ctx)
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
apeEnabled := bktInfo.APEEnabled
|
|
|
|
if !apeEnabled {
|
|
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
|
return
|
|
}
|
|
apeEnabled = len(settings.CannedACL) != 0
|
|
}
|
|
|
|
if apeEnabled {
|
|
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
|
return
|
|
}
|
|
|
|
versionID := reqInfo.URL.Query().Get(api.QueryVersionID)
|
|
key, err := h.bearerTokenIssuerKey(ctx)
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get gate key", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
token, err := getSessionTokenSetEACL(ctx)
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get eacl token", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
p := &layer.HeadObjectParams{
|
|
BktInfo: bktInfo,
|
|
Object: reqInfo.ObjectName,
|
|
VersionID: versionID,
|
|
}
|
|
|
|
objInfo, err := h.obj.GetObjectInfo(ctx, p)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not get object info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
list := &AccessControlPolicy{}
|
|
if r.ContentLength == 0 {
|
|
list, err = parseACLHeaders(r.Header, key)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not parse bucket acl", reqInfo, err)
|
|
return
|
|
}
|
|
} else if err = h.cfg.NewXMLDecoder(r.Body).Decode(list); err != nil {
|
|
h.logAndSendError(w, "could not parse bucket acl", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
|
|
return
|
|
}
|
|
|
|
resInfo := &resourceInfo{
|
|
Bucket: reqInfo.BucketName,
|
|
Object: reqInfo.ObjectName,
|
|
Version: objInfo.VersionID(),
|
|
}
|
|
|
|
astObject, err := aclToAst(list, resInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not translate acl to ast", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
updated, err := h.updateBucketACL(r, astObject, bktInfo, token)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not update bucket acl", reqInfo, err)
|
|
return
|
|
}
|
|
if updated {
|
|
s := &SendNotificationParams{
|
|
Event: EventObjectACLPut,
|
|
NotificationInfo: data.NotificationInfoFromObject(objInfo, h.cfg.MD5Enabled()),
|
|
BktInfo: bktInfo,
|
|
ReqInfo: reqInfo,
|
|
}
|
|
if err = h.sendNotifications(ctx, s); err != nil {
|
|
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
|
}
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
reqInfo := middleware.GetReqInfo(r.Context())
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
jsonPolicy, err := h.ape.GetBucketPolicy(reqInfo.Namespace, bktInfo.CID)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "not found") {
|
|
err = fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrNoSuchBucketPolicy), err.Error())
|
|
}
|
|
h.logAndSendError(w, "failed to get policy from storage", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
w.Header().Set(api.ContentType, "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
if _, err = w.Write(jsonPolicy); err != nil {
|
|
h.logAndSendError(w, "write json policy to client", reqInfo, err)
|
|
}
|
|
}
|
|
|
|
func (h *handler) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
reqInfo := middleware.GetReqInfo(r.Context())
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, getBucketChainID(chain.S3, bktInfo)); err != nil {
|
|
h.logAndSendError(w, "failed to delete policy from storage", reqInfo, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func checkOwner(info *data.BucketInfo, owner string) error {
|
|
if owner == "" {
|
|
return nil
|
|
}
|
|
|
|
// may need to convert owner to appropriate format
|
|
if info.Owner.String() != owner {
|
|
return fmt.Errorf("%w: mismatch owner", errors.GetAPIError(errors.ErrAccessDenied))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
reqInfo := middleware.GetReqInfo(r.Context())
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
jsonPolicy, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
h.logAndSendError(w, "read body", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
var bktPolicy engineiam.Policy
|
|
if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil {
|
|
h.logAndSendError(w, "could not parse bucket policy", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
for _, stat := range bktPolicy.Statement {
|
|
if len(stat.NotResource) != 0 {
|
|
h.logAndSendError(w, "policy resource mismatched bucket", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicy))
|
|
return
|
|
}
|
|
|
|
for _, resource := range stat.Resource {
|
|
if reqInfo.BucketName != strings.Split(strings.TrimPrefix(resource, arnAwsPrefix), "/")[0] {
|
|
h.logAndSendError(w, "policy resource mismatched bucket", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicy))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
nativeChain, err := engineiam.ConvertToNativeChain(bktPolicy, h.nativeResolver(reqInfo.Namespace, bktInfo))
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not convert s3 policy to native chain policy", reqInfo, err)
|
|
return
|
|
}
|
|
nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo)
|
|
|
|
s3Chain, err := engineiam.ConvertToS3Chain(bktPolicy, h.frostfsid)
|
|
if err != nil {
|
|
h.logAndSendError(w, "could not convert s3 policy to chain policy", reqInfo, err)
|
|
return
|
|
}
|
|
s3Chain.ID = getBucketChainID(chain.S3, bktInfo)
|
|
|
|
if err = h.ape.PutBucketPolicy(reqInfo.Namespace, bktInfo.CID, jsonPolicy, []*chain.Chain{s3Chain, nativeChain}); err != nil {
|
|
h.logAndSendError(w, "failed to update policy in contract", reqInfo, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
type nativeResolver struct {
|
|
FrostFSID
|
|
namespace string
|
|
bktInfo *data.BucketInfo
|
|
}
|
|
|
|
func (n *nativeResolver) GetBucketInfo(bucket string) (*engineiam.BucketInfo, error) {
|
|
if n.bktInfo.Name != bucket {
|
|
return nil, fmt.Errorf("invalid bucket %s: %w", bucket, errors.GetAPIError(errors.ErrMalformedPolicy))
|
|
}
|
|
|
|
return &engineiam.BucketInfo{Namespace: n.namespace, Container: n.bktInfo.CID.EncodeToString()}, nil
|
|
}
|
|
|
|
func (h *handler) nativeResolver(ns string, bktInfo *data.BucketInfo) engineiam.NativeResolver {
|
|
return &nativeResolver{
|
|
FrostFSID: h.frostfsid,
|
|
namespace: ns,
|
|
bktInfo: bktInfo,
|
|
}
|
|
}
|
|
|
|
func getBucketChainID(prefix chain.Name, bktInfo *data.BucketInfo) chain.ID {
|
|
return chain.ID(string(prefix) + ":bkt" + string(bktInfo.CID[:]))
|
|
}
|
|
|
|
func parseACLHeaders(header http.Header, key *keys.PublicKey) (*AccessControlPolicy, error) {
|
|
var err error
|
|
acp := &AccessControlPolicy{Owner: Owner{
|
|
ID: hex.EncodeToString(key.Bytes()),
|
|
DisplayName: key.Address(),
|
|
}}
|
|
acp.AccessControlList = []*Grant{{
|
|
Grantee: &Grantee{
|
|
ID: hex.EncodeToString(key.Bytes()),
|
|
DisplayName: key.Address(),
|
|
Type: acpCanonicalUser,
|
|
},
|
|
Permission: aclFullControl,
|
|
}}
|
|
|
|
cannedACL := header.Get(api.AmzACL)
|
|
if cannedACL != "" {
|
|
return addPredefinedACP(acp, cannedACL)
|
|
}
|
|
|
|
if acp.AccessControlList, err = addGrantees(acp.AccessControlList, header, api.AmzGrantFullControl); err != nil {
|
|
return nil, fmt.Errorf("add grantees full control: %w", err)
|
|
}
|
|
if acp.AccessControlList, err = addGrantees(acp.AccessControlList, header, api.AmzGrantRead); err != nil {
|
|
return nil, fmt.Errorf("add grantees read: %w", err)
|
|
}
|
|
if acp.AccessControlList, err = addGrantees(acp.AccessControlList, header, api.AmzGrantWrite); err != nil {
|
|
return nil, fmt.Errorf("add grantees write: %w", err)
|
|
}
|
|
|
|
return acp, nil
|
|
}
|
|
|
|
func addGrantees(list []*Grant, headers http.Header, hdr string) ([]*Grant, error) {
|
|
grant := headers.Get(hdr)
|
|
if grant == "" {
|
|
return list, nil
|
|
}
|
|
|
|
permission, err := grantHdrToPermission(hdr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse header: %w", err)
|
|
}
|
|
|
|
grantees, err := parseGrantee(grant)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse grantee: %w", err)
|
|
}
|
|
|
|
for _, grantee := range grantees {
|
|
if grantee.Type == acpAmazonCustomerByEmail || (grantee.Type == acpGroup && grantee.URI != allUsersGroup) {
|
|
return nil, stderrors.New("unsupported grantee type")
|
|
}
|
|
|
|
list = append(list, &Grant{
|
|
Grantee: grantee,
|
|
Permission: permission,
|
|
})
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
func grantHdrToPermission(grant string) (AWSACL, error) {
|
|
switch grant {
|
|
case api.AmzGrantFullControl:
|
|
return aclFullControl, nil
|
|
case api.AmzGrantRead:
|
|
return aclRead, nil
|
|
case api.AmzGrantWrite:
|
|
return aclWrite, nil
|
|
}
|
|
return "", fmt.Errorf("unsuppoted header: %s", grant)
|
|
}
|
|
|
|
func parseGrantee(grantees string) ([]*Grantee, error) {
|
|
var result []*Grantee
|
|
|
|
split := strings.Split(grantees, ", ")
|
|
for _, pair := range split {
|
|
split2 := strings.Split(pair, "=")
|
|
if len(split2) != 2 {
|
|
return nil, errors.GetAPIError(errors.ErrInvalidArgument)
|
|
}
|
|
|
|
grantee, err := formGrantee(split2[0], split2[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("form grantee: %w", err)
|
|
}
|
|
result = append(result, grantee)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func formGrantee(granteeType, value string) (*Grantee, error) {
|
|
value = data.UnQuote(value)
|
|
switch granteeType {
|
|
case "id":
|
|
return &Grantee{
|
|
ID: value,
|
|
Type: acpCanonicalUser,
|
|
}, nil
|
|
case "uri":
|
|
return &Grantee{
|
|
URI: value,
|
|
Type: acpGroup,
|
|
}, nil
|
|
case "emailAddress":
|
|
return &Grantee{
|
|
EmailAddress: value,
|
|
Type: acpAmazonCustomerByEmail,
|
|
}, nil
|
|
}
|
|
// do not return grantee type to avoid sensitive data logging (#489)
|
|
return nil, fmt.Errorf("unknown grantee type")
|
|
}
|
|
|
|
func addPredefinedACP(acp *AccessControlPolicy, cannedACL string) (*AccessControlPolicy, error) {
|
|
switch cannedACL {
|
|
case basicACLPrivate:
|
|
case basicACLPublic:
|
|
acp.AccessControlList = append(acp.AccessControlList, &Grant{
|
|
Grantee: &Grantee{
|
|
URI: allUsersGroup,
|
|
Type: acpGroup,
|
|
},
|
|
Permission: aclFullControl,
|
|
})
|
|
case cannedACLAuthRead:
|
|
fallthrough
|
|
case basicACLReadOnly:
|
|
acp.AccessControlList = append(acp.AccessControlList, &Grant{
|
|
Grantee: &Grantee{
|
|
URI: allUsersGroup,
|
|
Type: acpGroup,
|
|
},
|
|
Permission: aclRead,
|
|
})
|
|
default:
|
|
return nil, errors.GetAPIError(errors.ErrInvalidArgument)
|
|
}
|
|
|
|
return acp, nil
|
|
}
|
|
|
|
func tableToAst(table *eacl.Table, bktName string) *ast {
|
|
resourceMap := make(map[string]orderedAstResource)
|
|
|
|
var groupRecordsLeft int
|
|
var currentResource orderedAstResource
|
|
for i, record := range table.Records() {
|
|
if serviceRec := tryServiceRecord(record); serviceRec != nil {
|
|
resInfo := resourceInfoFromName(serviceRec.Resource, bktName)
|
|
groupRecordsLeft = serviceRec.GroupRecordsLength
|
|
|
|
currentResource = getResourceOrCreate(resourceMap, i, resInfo)
|
|
resourceMap[resInfo.Name()] = currentResource
|
|
} else if groupRecordsLeft != 0 {
|
|
groupRecordsLeft--
|
|
addOperationsAndUpdateMap(currentResource, record, resourceMap)
|
|
} else {
|
|
resInfo := resInfoFromFilters(bktName, record.Filters())
|
|
resource := getResourceOrCreate(resourceMap, i, resInfo)
|
|
addOperationsAndUpdateMap(resource, record, resourceMap)
|
|
}
|
|
}
|
|
|
|
return &ast{
|
|
Resources: formReverseOrderResources(resourceMap),
|
|
}
|
|
}
|
|
|
|
func formReverseOrderResources(resourceMap map[string]orderedAstResource) []*astResource {
|
|
orderedResources := make([]orderedAstResource, 0, len(resourceMap))
|
|
for _, resource := range resourceMap {
|
|
orderedResources = append(orderedResources, resource)
|
|
}
|
|
sort.Slice(orderedResources, func(i, j int) bool {
|
|
return orderedResources[i].Index >= orderedResources[j].Index // reverse order
|
|
})
|
|
|
|
result := make([]*astResource, len(orderedResources))
|
|
for i, ordered := range orderedResources {
|
|
res := ordered.Resource
|
|
for j, k := 0, len(res.Operations)-1; j < k; j, k = j+1, k-1 {
|
|
res.Operations[j], res.Operations[k] = res.Operations[k], res.Operations[j]
|
|
}
|
|
|
|
result[i] = res
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func addOperationsAndUpdateMap(orderedRes orderedAstResource, record eacl.Record, resMap map[string]orderedAstResource) {
|
|
for _, target := range record.Targets() {
|
|
orderedRes.Resource.Operations = addToList(orderedRes.Resource.Operations, record, target)
|
|
}
|
|
resMap[orderedRes.Resource.Name()] = orderedRes
|
|
}
|
|
|
|
func getResourceOrCreate(resMap map[string]orderedAstResource, index int, resInfo resourceInfo) orderedAstResource {
|
|
resource, ok := resMap[resInfo.Name()]
|
|
if !ok {
|
|
resource = orderedAstResource{
|
|
Index: index,
|
|
Resource: &astResource{resourceInfo: resInfo},
|
|
}
|
|
}
|
|
return resource
|
|
}
|
|
|
|
func resInfoFromFilters(bucketName string, filters []eacl.Filter) resourceInfo {
|
|
resInfo := resourceInfo{Bucket: bucketName}
|
|
for _, filter := range filters {
|
|
if filter.Matcher() == eacl.MatchStringEqual {
|
|
if filter.Key() == object.AttributeFilePath {
|
|
resInfo.Object = filter.Value()
|
|
} else if filter.Key() == v2acl.FilterObjectID {
|
|
resInfo.Version = filter.Value()
|
|
}
|
|
}
|
|
}
|
|
|
|
return resInfo
|
|
}
|
|
|
|
func mergeAst(parent, child *ast) (*ast, bool) {
|
|
updated := false
|
|
for _, resource := range child.Resources {
|
|
parentResource := getParentResource(parent, resource)
|
|
if parentResource == nil {
|
|
parent.Resources = append(parent.Resources, resource)
|
|
updated = true
|
|
continue
|
|
}
|
|
|
|
var newOps []*astOperation
|
|
for _, astOp := range resource.Operations {
|
|
// get parent matched operations
|
|
ops := getAstOps(parentResource, astOp)
|
|
switch len(ops) {
|
|
case 2: // parent contains different actions for the same child operation
|
|
// potential inconsistency
|
|
if groupGrantee := astOp.IsGroupGrantee(); groupGrantee {
|
|
// it is not likely (such state must be detected early)
|
|
// inconsistency
|
|
action := eacl.ActionAllow
|
|
if astOp.Action == eacl.ActionAllow {
|
|
action = eacl.ActionDeny
|
|
}
|
|
removeAstOp(parentResource, groupGrantee, astOp.Op, action)
|
|
updated = true
|
|
continue
|
|
}
|
|
|
|
opToAdd, opToDelete := ops[0], ops[1]
|
|
if ops[1].Action == astOp.Action {
|
|
opToAdd, opToDelete = ops[1], ops[0]
|
|
}
|
|
|
|
if handleAddOperations(parentResource, astOp, opToAdd) {
|
|
updated = true
|
|
}
|
|
if handleRemoveOperations(parentResource, astOp, opToDelete) {
|
|
updated = true
|
|
}
|
|
case 1: // parent contains some action for the same child operation
|
|
if astOp.Action != ops[0].Action {
|
|
// potential inconsistency
|
|
if groupGrantee := astOp.IsGroupGrantee(); groupGrantee {
|
|
// inconsistency
|
|
ops[0].Action = astOp.Action
|
|
updated = true
|
|
continue
|
|
}
|
|
|
|
if handleRemoveOperations(parentResource, astOp, ops[0]) {
|
|
updated = true
|
|
}
|
|
parentResource.Operations = append(parentResource.Operations, astOp)
|
|
continue
|
|
}
|
|
|
|
if handleAddOperations(parentResource, astOp, ops[0]) {
|
|
updated = true
|
|
}
|
|
case 0: // parent doesn't contain actions for the same child operation
|
|
newOps = append(newOps, astOp)
|
|
updated = true
|
|
}
|
|
}
|
|
|
|
if newOps != nil {
|
|
parentResource.Operations = append(newOps, parentResource.Operations...)
|
|
}
|
|
}
|
|
|
|
return parent, updated
|
|
}
|
|
|
|
func handleAddOperations(parentResource *astResource, astOp, existedOp *astOperation) bool {
|
|
var needToAdd []string
|
|
for _, user := range astOp.Users {
|
|
if !containsStr(existedOp.Users, user) {
|
|
needToAdd = append(needToAdd, user)
|
|
}
|
|
}
|
|
if len(needToAdd) != 0 {
|
|
addUsers(parentResource, existedOp, needToAdd)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func handleRemoveOperations(parentResource *astResource, astOp, existedOp *astOperation) bool {
|
|
var needToRemove []string
|
|
for _, user := range astOp.Users {
|
|
if containsStr(existedOp.Users, user) {
|
|
needToRemove = append(needToRemove, user)
|
|
}
|
|
}
|
|
if len(needToRemove) != 0 {
|
|
removeUsers(parentResource, existedOp, needToRemove)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func containsStr(list []string, element string) bool {
|
|
for _, str := range list {
|
|
if str == element {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getAstOps(resource *astResource, childOp *astOperation) []*astOperation {
|
|
var res []*astOperation
|
|
for _, astOp := range resource.Operations {
|
|
if astOp.IsGroupGrantee() == childOp.IsGroupGrantee() && astOp.Op == childOp.Op {
|
|
res = append(res, astOp)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
func removeAstOp(resource *astResource, group bool, op eacl.Operation, action eacl.Action) {
|
|
for i, astOp := range resource.Operations {
|
|
if astOp.IsGroupGrantee() == group && astOp.Op == op && astOp.Action == action {
|
|
resource.Operations = append(resource.Operations[:i], resource.Operations[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func addUsers(resource *astResource, astO *astOperation, users []string) {
|
|
for _, astOp := range resource.Operations {
|
|
if astOp.IsGroupGrantee() == astO.IsGroupGrantee() && astOp.Op == astO.Op && astOp.Action == astO.Action {
|
|
astOp.Users = append(astO.Users, users...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func removeUsers(resource *astResource, astOperation *astOperation, users []string) {
|
|
for ind, astOp := range resource.Operations {
|
|
if !astOp.IsGroupGrantee() && astOp.Op == astOperation.Op && astOp.Action == astOperation.Action {
|
|
filteredUsers := astOp.Users[:0] // new slice without allocation
|
|
for _, user := range astOp.Users {
|
|
if !containsStr(users, user) {
|
|
filteredUsers = append(filteredUsers, user)
|
|
}
|
|
}
|
|
if len(filteredUsers) == 0 { // remove ast resource
|
|
resource.Operations = append(resource.Operations[:ind], resource.Operations[ind+1:]...)
|
|
} else {
|
|
astOp.Users = filteredUsers
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func getParentResource(parent *ast, resource *astResource) *astResource {
|
|
for _, parentResource := range parent.Resources {
|
|
if resource.Bucket == parentResource.Bucket && resource.Object == parentResource.Object &&
|
|
resource.Version == parentResource.Version {
|
|
return parentResource
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func astToTable(ast *ast) (*eacl.Table, error) {
|
|
table := eacl.NewTable()
|
|
|
|
for i := len(ast.Resources) - 1; i >= 0; i-- {
|
|
records, err := formRecords(ast.Resources[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("form records: %w", err)
|
|
}
|
|
|
|
serviceRecord := ServiceRecord{
|
|
Resource: ast.Resources[i].Name(),
|
|
GroupRecordsLength: len(records),
|
|
}
|
|
table.AddRecord(serviceRecord.ToEACLRecord())
|
|
|
|
for _, rec := range records {
|
|
table.AddRecord(rec)
|
|
}
|
|
}
|
|
|
|
return table, nil
|
|
}
|
|
|
|
func tryServiceRecord(record eacl.Record) *ServiceRecord {
|
|
if record.Action() != eacl.ActionAllow || record.Operation() != eacl.OperationGet ||
|
|
len(record.Targets()) != 1 || len(record.Filters()) != 2 {
|
|
return nil
|
|
}
|
|
|
|
target := record.Targets()[0]
|
|
if target.Role() != eacl.RoleSystem {
|
|
return nil
|
|
}
|
|
|
|
resourceFilter := record.Filters()[0]
|
|
recordsFilter := record.Filters()[1]
|
|
if resourceFilter.From() != eacl.HeaderFromService || recordsFilter.From() != eacl.HeaderFromService ||
|
|
resourceFilter.Matcher() != eacl.MatchUnknown || recordsFilter.Matcher() != eacl.MatchUnknown ||
|
|
resourceFilter.Key() != serviceRecordResourceKey || recordsFilter.Key() != serviceRecordGroupLengthKey {
|
|
return nil
|
|
}
|
|
|
|
groupLength, err := strconv.Atoi(recordsFilter.Value())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return &ServiceRecord{
|
|
Resource: resourceFilter.Value(),
|
|
GroupRecordsLength: groupLength,
|
|
}
|
|
}
|
|
|
|
func formRecords(resource *astResource) ([]*eacl.Record, error) {
|
|
var res []*eacl.Record
|
|
|
|
for i := len(resource.Operations) - 1; i >= 0; i-- {
|
|
astOp := resource.Operations[i]
|
|
record := eacl.NewRecord()
|
|
record.SetOperation(astOp.Op)
|
|
record.SetAction(astOp.Action)
|
|
if astOp.IsGroupGrantee() {
|
|
eacl.AddFormedTarget(record, eacl.RoleOthers)
|
|
} else {
|
|
targetKeys := make([]ecdsa.PublicKey, 0, len(astOp.Users))
|
|
for _, user := range astOp.Users {
|
|
pk, err := keys.NewPublicKeyFromString(user)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("public key from string: %w", err)
|
|
}
|
|
targetKeys = append(targetKeys, (ecdsa.PublicKey)(*pk))
|
|
}
|
|
// Unknown role is used, because it is ignored when keys are set
|
|
eacl.AddFormedTarget(record, eacl.RoleUnknown, targetKeys...)
|
|
}
|
|
if len(resource.Object) != 0 {
|
|
if len(resource.Version) != 0 {
|
|
var id oid.ID
|
|
if err := id.DecodeString(resource.Version); err != nil {
|
|
return nil, fmt.Errorf("parse object version (oid): %w", err)
|
|
}
|
|
record.AddObjectIDFilter(eacl.MatchStringEqual, id)
|
|
} else {
|
|
record.AddObjectAttributeFilter(eacl.MatchStringEqual, object.AttributeFilePath, resource.Object)
|
|
}
|
|
}
|
|
res = append(res, record)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func addToList(operations []*astOperation, rec eacl.Record, target eacl.Target) []*astOperation {
|
|
var (
|
|
found *astOperation
|
|
groupTarget = target.Role() == eacl.RoleOthers
|
|
)
|
|
|
|
for _, astOp := range operations {
|
|
if astOp.Op == rec.Operation() && astOp.IsGroupGrantee() == groupTarget {
|
|
found = astOp
|
|
}
|
|
}
|
|
|
|
if found != nil {
|
|
if !groupTarget {
|
|
for _, key := range target.BinaryKeys() {
|
|
found.Users = append(found.Users, hex.EncodeToString(key))
|
|
}
|
|
}
|
|
} else {
|
|
astOperation := &astOperation{
|
|
Op: rec.Operation(),
|
|
Action: rec.Action(),
|
|
}
|
|
if !groupTarget {
|
|
for _, key := range target.BinaryKeys() {
|
|
astOperation.Users = append(astOperation.Users, hex.EncodeToString(key))
|
|
}
|
|
}
|
|
|
|
operations = append(operations, astOperation)
|
|
}
|
|
|
|
return operations
|
|
}
|
|
|
|
func policyToAst(bktPolicy *bucketPolicy) (*ast, error) {
|
|
res := &ast{}
|
|
|
|
rr := make(map[string]*astResource)
|
|
|
|
for _, state := range bktPolicy.Statement {
|
|
if state.Principal.AWS != "" && state.Principal.AWS != allUsersWildcard ||
|
|
state.Principal.AWS == "" && state.Principal.CanonicalUser == "" {
|
|
return nil, fmt.Errorf("unsupported principal: %v", state.Principal)
|
|
}
|
|
var groupGrantee bool
|
|
if state.Principal.AWS == allUsersWildcard {
|
|
groupGrantee = true
|
|
}
|
|
|
|
for _, resource := range state.Resource {
|
|
trimmedResource := strings.TrimPrefix(resource, arnAwsPrefix)
|
|
r, ok := rr[trimmedResource]
|
|
if !ok {
|
|
if !strings.HasPrefix(trimmedResource, bktPolicy.Bucket) {
|
|
return nil, fmt.Errorf("resource '%s' must be in the same bucket '%s'", trimmedResource, bktPolicy.Bucket)
|
|
}
|
|
|
|
r = &astResource{
|
|
resourceInfo: resourceInfoFromName(trimmedResource, bktPolicy.Bucket),
|
|
}
|
|
}
|
|
for _, action := range state.Action {
|
|
for _, op := range actionToOpMap[action] {
|
|
toAction := effectToAction(state.Effect)
|
|
r.Operations = addTo(r.Operations, state.Principal.CanonicalUser, op, groupGrantee, toAction)
|
|
}
|
|
}
|
|
|
|
rr[trimmedResource] = r
|
|
}
|
|
}
|
|
|
|
for _, val := range rr {
|
|
res.Resources = append(res.Resources, val)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func resourceInfoFromName(name, bucketName string) resourceInfo {
|
|
resInfo := resourceInfo{Bucket: bucketName}
|
|
if name != bucketName {
|
|
versionedObject := strings.TrimPrefix(name, bucketName+"/")
|
|
objVersion := strings.Split(versionedObject, ":")
|
|
if len(objVersion) <= 2 {
|
|
resInfo.Object = objVersion[0]
|
|
if len(objVersion) == 2 {
|
|
resInfo.Version = objVersion[1]
|
|
}
|
|
} else {
|
|
resInfo.Object = strings.Join(objVersion[:len(objVersion)-1], ":")
|
|
resInfo.Version = objVersion[len(objVersion)-1]
|
|
}
|
|
}
|
|
|
|
return resInfo
|
|
}
|
|
|
|
func addTo(list []*astOperation, userID string, op eacl.Operation, groupGrantee bool, action eacl.Action) []*astOperation {
|
|
var found *astOperation
|
|
for _, astop := range list {
|
|
if astop.Op == op && astop.IsGroupGrantee() == groupGrantee {
|
|
found = astop
|
|
}
|
|
}
|
|
|
|
if found != nil {
|
|
if !groupGrantee {
|
|
found.Users = append(found.Users, userID)
|
|
}
|
|
} else {
|
|
astoperation := &astOperation{
|
|
Op: op,
|
|
Action: action,
|
|
}
|
|
if !groupGrantee {
|
|
astoperation.Users = append(astoperation.Users, userID)
|
|
}
|
|
|
|
list = append(list, astoperation)
|
|
}
|
|
|
|
return list
|
|
}
|
|
|
|
func aclToAst(acl *AccessControlPolicy, resInfo *resourceInfo) (*ast, error) {
|
|
res := &ast{}
|
|
|
|
resource := &astResource{resourceInfo: *resInfo}
|
|
|
|
ops := readOps
|
|
if resInfo.IsBucket() {
|
|
ops = append(ops, writeOps...)
|
|
}
|
|
|
|
// Expect to have at least 1 full control grant for owner which is set in
|
|
// parseACLHeaders(). If there is no other grants, then user sets private
|
|
// canned ACL, which is processed in this branch.
|
|
if len(acl.AccessControlList) < 2 {
|
|
for _, op := range ops {
|
|
operation := &astOperation{
|
|
Op: op,
|
|
Action: eacl.ActionDeny,
|
|
}
|
|
resource.Operations = append(resource.Operations, operation)
|
|
}
|
|
}
|
|
|
|
for _, op := range ops {
|
|
operation := &astOperation{
|
|
Users: []string{acl.Owner.ID},
|
|
Op: op,
|
|
Action: eacl.ActionAllow,
|
|
}
|
|
resource.Operations = append(resource.Operations, operation)
|
|
}
|
|
|
|
for _, grant := range acl.AccessControlList {
|
|
if grant.Grantee.Type == acpAmazonCustomerByEmail || (grant.Grantee.Type == acpGroup && grant.Grantee.URI != allUsersGroup) {
|
|
return nil, stderrors.New("unsupported grantee type")
|
|
}
|
|
|
|
var groupGrantee bool
|
|
if grant.Grantee.Type == acpGroup {
|
|
groupGrantee = true
|
|
} else if grant.Grantee.ID == acl.Owner.ID {
|
|
continue
|
|
}
|
|
|
|
for _, action := range getActions(grant.Permission, resInfo.IsBucket()) {
|
|
for _, op := range actionToOpMap[action] {
|
|
resource.Operations = addTo(resource.Operations, grant.Grantee.ID, op, groupGrantee, eacl.ActionAllow)
|
|
}
|
|
}
|
|
}
|
|
|
|
res.Resources = []*astResource{resource}
|
|
return res, nil
|
|
}
|
|
|
|
func aclToPolicy(acl *AccessControlPolicy, resInfo *resourceInfo) (*bucketPolicy, error) {
|
|
if resInfo.Bucket == "" {
|
|
return nil, fmt.Errorf("resource bucket must not be empty")
|
|
}
|
|
|
|
results := []statement{
|
|
getAllowStatement(resInfo, acl.Owner.ID, aclFullControl),
|
|
}
|
|
|
|
// Expect to have at least 1 full control grant for owner which is set in
|
|
// parseACLHeaders(). If there is no other grants, then user sets private
|
|
// canned ACL, which is processed in this branch.
|
|
if len(acl.AccessControlList) < 2 {
|
|
results = append([]statement{getDenyStatement(resInfo, allUsersWildcard, aclFullControl)}, results...)
|
|
}
|
|
|
|
for _, grant := range acl.AccessControlList {
|
|
if grant.Grantee.Type == acpAmazonCustomerByEmail || (grant.Grantee.Type == acpGroup && grant.Grantee.URI != allUsersGroup) {
|
|
return nil, stderrors.New("unsupported grantee type")
|
|
}
|
|
|
|
user := grant.Grantee.ID
|
|
if grant.Grantee.Type == acpGroup {
|
|
user = allUsersWildcard
|
|
} else if user == acl.Owner.ID {
|
|
continue
|
|
}
|
|
results = append(results, getAllowStatement(resInfo, user, grant.Permission))
|
|
}
|
|
|
|
return &bucketPolicy{
|
|
Statement: results,
|
|
Bucket: resInfo.Bucket,
|
|
}, nil
|
|
}
|
|
|
|
func getAllowStatement(resInfo *resourceInfo, id string, permission AWSACL) statement {
|
|
state := statement{
|
|
Effect: "Allow",
|
|
Principal: principal{
|
|
CanonicalUser: id,
|
|
},
|
|
Action: getActions(permission, resInfo.IsBucket()),
|
|
Resource: []string{arnAwsPrefix + resInfo.Name()},
|
|
}
|
|
|
|
if id == allUsersWildcard {
|
|
state.Principal = principal{AWS: allUsersWildcard}
|
|
}
|
|
|
|
return state
|
|
}
|
|
|
|
func getDenyStatement(resInfo *resourceInfo, id string, permission AWSACL) statement {
|
|
state := statement{
|
|
Effect: "Deny",
|
|
Principal: principal{
|
|
CanonicalUser: id,
|
|
},
|
|
Action: getActions(permission, resInfo.IsBucket()),
|
|
Resource: []string{arnAwsPrefix + resInfo.Name()},
|
|
}
|
|
|
|
if id == allUsersWildcard {
|
|
state.Principal = principal{AWS: allUsersWildcard}
|
|
}
|
|
|
|
return state
|
|
}
|
|
|
|
func getActions(permission AWSACL, isBucket bool) []string {
|
|
var res []string
|
|
switch permission {
|
|
case aclRead:
|
|
if isBucket {
|
|
res = []string{s3ListBucket, s3ListBucketVersions, s3ListBucketMultipartUploads}
|
|
} else {
|
|
res = []string{s3GetObject, s3GetObjectVersion}
|
|
}
|
|
case aclWrite:
|
|
if isBucket {
|
|
res = []string{s3PutObject, s3DeleteObject}
|
|
}
|
|
case aclFullControl:
|
|
if isBucket {
|
|
res = []string{s3ListBucket, s3ListBucketVersions, s3ListBucketMultipartUploads, s3PutObject, s3DeleteObject}
|
|
} else {
|
|
res = []string{s3GetObject, s3GetObjectVersion}
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func effectToAction(effect string) eacl.Action {
|
|
switch effect {
|
|
case "Allow":
|
|
return eacl.ActionAllow
|
|
case "Deny":
|
|
return eacl.ActionDeny
|
|
}
|
|
return eacl.ActionUnknown
|
|
}
|
|
|
|
func permissionToOperations(permission AWSACL) []eacl.Operation {
|
|
switch permission {
|
|
case aclFullControl:
|
|
return fullOps
|
|
case aclRead:
|
|
return readOps
|
|
case aclWrite:
|
|
return writeOps
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isWriteOperation(op eacl.Operation) bool {
|
|
return op == eacl.OperationDelete || op == eacl.OperationPut
|
|
}
|
|
|
|
func (h *handler) encodeObjectACL(ctx context.Context, bucketACL *layer.BucketACL, bucketName, objectVersion string) *AccessControlPolicy {
|
|
res := &AccessControlPolicy{
|
|
Owner: Owner{
|
|
ID: bucketACL.Info.Owner.String(),
|
|
DisplayName: bucketACL.Info.Owner.String(),
|
|
},
|
|
}
|
|
|
|
m := make(map[string][]eacl.Operation)
|
|
|
|
astList := tableToAst(bucketACL.EACL, bucketName)
|
|
|
|
for _, resource := range astList.Resources {
|
|
if resource.Version != objectVersion {
|
|
continue
|
|
}
|
|
|
|
for _, op := range resource.Operations {
|
|
if op.Action != eacl.ActionAllow {
|
|
continue
|
|
}
|
|
|
|
if len(op.Users) == 0 {
|
|
list := append(m[allUsersGroup], op.Op)
|
|
m[allUsersGroup] = list
|
|
} else {
|
|
for _, user := range op.Users {
|
|
list := append(m[user], op.Op)
|
|
m[user] = list
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for key, val := range m {
|
|
permission := aclFullControl
|
|
read := true
|
|
for op := eacl.OperationGet; op <= eacl.OperationRangeHash; op++ {
|
|
if !contains(val, op) && !isWriteOperation(op) {
|
|
read = false
|
|
}
|
|
}
|
|
|
|
if read {
|
|
permission = aclFullControl
|
|
} else {
|
|
h.reqLogger(ctx).Warn(logs.SomeACLNotFullyMapped)
|
|
}
|
|
|
|
var grantee *Grantee
|
|
if key == allUsersGroup {
|
|
grantee = NewGrantee(acpGroup)
|
|
grantee.URI = allUsersGroup
|
|
} else {
|
|
grantee = NewGrantee(acpCanonicalUser)
|
|
grantee.ID = key
|
|
}
|
|
|
|
grant := &Grant{
|
|
Grantee: grantee,
|
|
Permission: permission,
|
|
}
|
|
res.AccessControlList = append(res.AccessControlList, grant)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func (h *handler) encodeBucketACL(ctx context.Context, bucketName string, bucketACL *layer.BucketACL) *AccessControlPolicy {
|
|
return h.encodeObjectACL(ctx, bucketACL, bucketName, "")
|
|
}
|
|
|
|
func contains(list []eacl.Operation, op eacl.Operation) bool {
|
|
for _, operation := range list {
|
|
if operation == op {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type getRecordFunc func(op eacl.Operation) *eacl.Record
|
|
|
|
func bucketACLToTable(acp *AccessControlPolicy, resInfo *resourceInfo) (*eacl.Table, error) {
|
|
if !resInfo.IsBucket() {
|
|
return nil, fmt.Errorf("allowed only bucket acl")
|
|
}
|
|
|
|
var found bool
|
|
table := eacl.NewTable()
|
|
|
|
ownerKey, err := keys.NewPublicKeyFromString(acp.Owner.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("public key from string: %w", err)
|
|
}
|
|
|
|
for _, grant := range acp.AccessControlList {
|
|
if !isValidGrant(grant) {
|
|
return nil, stderrors.New("unsupported grantee")
|
|
}
|
|
if grant.Grantee.ID == acp.Owner.ID {
|
|
found = true
|
|
}
|
|
|
|
getRecord, err := getRecordFunction(grant.Grantee)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("record func from grantee: %w", err)
|
|
}
|
|
for _, op := range permissionToOperations(grant.Permission) {
|
|
table.AddRecord(getRecord(op))
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
for _, op := range fullOps {
|
|
table.AddRecord(getAllowRecord(op, ownerKey))
|
|
}
|
|
}
|
|
|
|
for _, op := range fullOps {
|
|
table.AddRecord(getOthersRecord(op, eacl.ActionDeny))
|
|
}
|
|
|
|
return table, nil
|
|
}
|
|
|
|
func getRecordFunction(grantee *Grantee) (getRecordFunc, error) {
|
|
switch grantee.Type {
|
|
case acpAmazonCustomerByEmail:
|
|
case acpCanonicalUser:
|
|
pk, err := keys.NewPublicKeyFromString(grantee.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't parse canonical ID %s: %w", grantee.ID, err)
|
|
}
|
|
return func(op eacl.Operation) *eacl.Record {
|
|
return getAllowRecord(op, pk)
|
|
}, nil
|
|
case acpGroup:
|
|
return func(op eacl.Operation) *eacl.Record {
|
|
return getOthersRecord(op, eacl.ActionAllow)
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown type: %s", grantee.Type)
|
|
}
|
|
|
|
func isValidGrant(grant *Grant) bool {
|
|
return (grant.Permission == aclFullControl || grant.Permission == aclRead || grant.Permission == aclWrite) &&
|
|
(grant.Grantee.Type == acpCanonicalUser || (grant.Grantee.Type == acpGroup && grant.Grantee.URI == allUsersGroup))
|
|
}
|
|
|
|
func getAllowRecord(op eacl.Operation, pk *keys.PublicKey) *eacl.Record {
|
|
record := eacl.NewRecord()
|
|
record.SetOperation(op)
|
|
record.SetAction(eacl.ActionAllow)
|
|
// Unknown role is used, because it is ignored when keys are set
|
|
eacl.AddFormedTarget(record, eacl.RoleUnknown, (ecdsa.PublicKey)(*pk))
|
|
return record
|
|
}
|
|
|
|
func getOthersRecord(op eacl.Operation, action eacl.Action) *eacl.Record {
|
|
record := eacl.NewRecord()
|
|
record.SetOperation(op)
|
|
record.SetAction(action)
|
|
eacl.AddFormedTarget(record, eacl.RoleOthers)
|
|
return record
|
|
}
|