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(ctx, w, "could not get bucket info", reqInfo, err) return } settings, err := h.obj.GetBucketSettings(ctx, bktInfo) if err != nil { h.logAndSendError(ctx, w, "couldn't get bucket settings", reqInfo, err) return } if err = middleware.EncodeToResponse(w, h.encodeBucketCannedACL(ctx, bktInfo, settings)); err != nil { h.logAndSendError(ctx, 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) { ctx := r.Context() reqInfo := middleware.GetReqInfo(ctx) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) if err != nil { h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err) return } settings, err := h.obj.GetBucketSettings(ctx, bktInfo) if err != nil { h.logAndSendError(ctx, 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(ctx).Warn(logs.CouldNotCloseRequestBody, zap.Error(errBody)) } }() written, err := io.Copy(io.Discard, r.Body) if err != nil { h.logAndSendError(ctx, w, "couldn't read request body", reqInfo, err) return } if written != 0 || len(r.Header.Get(api.AmzACL)) == 0 { h.logAndSendError(ctx, w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) return } cannedACL, err := parseCannedACL(r.Header) if err != nil { h.logAndSendError(ctx, 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(ctx, 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(ctx, 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(ctx, w, "could not get bucket info", reqInfo, err) return } settings, err := h.obj.GetBucketSettings(ctx, bktInfo) if err != nil { h.logAndSendError(ctx, w, "couldn't get bucket settings", reqInfo, err) return } if err = middleware.EncodeToResponse(w, h.encodePrivateCannedACL(ctx, bktInfo, settings)); err != nil { h.logAndSendError(ctx, 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(ctx, w, "could not get bucket info", reqInfo, err) return } h.logAndSendError(ctx, w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported)) } func (h *handler) GetBucketPolicyStatusHandler(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(ctx, 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(ctx, w, "failed to get policy from storage", reqInfo, err) return } var bktPolicy engineiam.Policy if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil { h.logAndSendError(ctx, 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(ctx, w, "encode and write response", reqInfo, err) return } } func (h *handler) GetBucketPolicyHandler(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(ctx, 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(ctx, 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(ctx, w, "write json policy to client", reqInfo, err) } } func (h *handler) DeleteBucketPolicyHandler(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(ctx, 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(ctx, 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) { ctx := r.Context() reqInfo := middleware.GetReqInfo(ctx) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) if err != nil { h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err) return } jsonPolicy, err := io.ReadAll(r.Body) if err != nil { h.logAndSendError(ctx, w, "read body", reqInfo, err) return } var bktPolicy engineiam.Policy if err = json.Unmarshal(jsonPolicy, &bktPolicy); err != nil { h.logAndSendError(ctx, w, "could not parse bucket policy", reqInfo, err) return } for _, stat := range bktPolicy.Statement { if len(stat.NotResource) != 0 { h.logAndSendError(ctx, w, "policy resource mismatched bucket", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicy)) return } if len(stat.NotPrincipal) != 0 && stat.Effect == engineiam.AllowEffect { h.logAndSendError(ctx, 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(ctx, w, "policy resource mismatched bucket", reqInfo, errors.GetAPIError(errors.ErrMalformedPolicy)) return } } } s3Chain, err := engineiam.ConvertToS3Chain(bktPolicy, h.frostfsid) if err != nil { h.logAndSendError(ctx, 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(ctx, w, "could not convert s3 policy to native chain policy", reqInfo, err) return } else { h.reqLogger(ctx).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(ctx, 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[:])) }