forked from TrueCloudLab/frostfs-s3-gw
Denis Kirillov
465eaa816a
Always consider buckets as APE compatible Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
418 lines
12 KiB
Go
418 lines
12 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"crypto/elliptic"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
stderrors "errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"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"
|
|
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"
|
|
)
|
|
|
|
const (
|
|
arnAwsPrefix = "arn:aws:s3:::"
|
|
allUsersGroup = "http://acs.amazonaws.com/groups/global/AllUsers"
|
|
)
|
|
|
|
// 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"
|
|
acpGroup GranteeType = "Group"
|
|
)
|
|
|
|
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 err = middleware.EncodeToResponse(w, h.encodeBucketCannedACL(ctx, bktInfo, settings)); 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 {
|
|
res := h.encodePrivateCannedACL(ctx, bktInfo, settings)
|
|
|
|
switch settings.CannedACL {
|
|
case basicACLPublic:
|
|
grantee := NewGrantee(acpGroup)
|
|
grantee.URI = allUsersGroup
|
|
|
|
res.AccessControlList = append(res.AccessControlList, &Grant{
|
|
Grantee: grantee,
|
|
Permission: aclWrite,
|
|
})
|
|
fallthrough
|
|
case basicACLReadOnly:
|
|
grantee := NewGrantee(acpGroup)
|
|
grantee.URI = allUsersGroup
|
|
|
|
res.AccessControlList = append(res.AccessControlList, &Grant{
|
|
Grantee: grantee,
|
|
Permission: aclRead,
|
|
})
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func (h *handler) encodePrivateCannedACL(ctx context.Context, bktInfo *data.BucketInfo, settings *data.BucketSettings) *AccessControlPolicy {
|
|
ownerDisplayName := bktInfo.Owner.EncodeToString()
|
|
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,
|
|
}}
|
|
|
|
return res
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
h.putBucketACLAPEHandler(w, r, reqInfo, bktInfo, settings)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
chainRules := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
|
|
if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), 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) 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
|
|
}
|
|
|
|
settings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
|
if err != nil {
|
|
h.logAndSendError(w, "couldn't get bucket settings", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
if err = middleware.EncodeToResponse(w, h.encodePrivateCannedACL(ctx, bktInfo, settings)); err != nil {
|
|
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
reqInfo := middleware.GetReqInfo(ctx)
|
|
|
|
if _, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName); err != nil {
|
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
h.logAndSendError(w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
|
}
|
|
|
|
func (h *handler) GetBucketPolicyStatusHandler(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
|
|
}
|
|
|
|
var bktPolicy engineiam.Policy
|
|
if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil {
|
|
h.logAndSendError(w, "could not parse bucket policy", reqInfo, err)
|
|
return
|
|
}
|
|
|
|
policyStatus := &PolicyStatus{
|
|
IsPublic: PolicyStatusIsPublicFalse,
|
|
}
|
|
|
|
for _, st := range bktPolicy.Statement {
|
|
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html#access-control-block-public-access-policy-status
|
|
if _, ok := st.Principal[engineiam.Wildcard]; ok {
|
|
policyStatus.IsPublic = PolicyStatusIsPublicTrue
|
|
break
|
|
}
|
|
}
|
|
|
|
if err = middleware.EncodeToResponse(w, policyStatus); err != nil {
|
|
h.logAndSendError(w, "encode and write response", reqInfo, err)
|
|
return
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
chainIDs := []chain.ID{getBucketChainID(chain.S3, bktInfo), getBucketChainID(chain.Ingress, bktInfo)}
|
|
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); 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
|
|
}
|
|
|
|
if len(stat.NotPrincipal) != 0 && stat.Effect == engineiam.AllowEffect {
|
|
h.logAndSendError(w, "invalid NotPrincipal", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicyNotPrincipal))
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
nativeChain, err := engineiam.ConvertToNativeChain(bktPolicy, h.nativeResolver(reqInfo.Namespace, bktInfo))
|
|
if err == nil {
|
|
nativeChain.ID = getBucketChainID(chain.Ingress, bktInfo)
|
|
} else if !stderrors.Is(err, engineiam.ErrActionsNotApplicable) {
|
|
h.logAndSendError(w, "could not convert s3 policy to native chain policy", reqInfo, err)
|
|
return
|
|
} else {
|
|
h.reqLogger(r.Context()).Warn(logs.PolicyCouldntBeConvertedToNativeRules)
|
|
}
|
|
|
|
chainsToSave := []*chain.Chain{s3Chain}
|
|
if nativeChain != nil {
|
|
chainsToSave = append(chainsToSave, nativeChain)
|
|
}
|
|
|
|
if err = h.ape.PutBucketPolicy(reqInfo.Namespace, bktInfo.CID, jsonPolicy, chainsToSave); 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[:]))
|
|
}
|