package middleware import ( "context" "crypto/elliptic" "fmt" "net/http" "strings" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" apiErr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" "git.frostfs.info/TrueCloudLab/policy-engine/schema/common" "git.frostfs.info/TrueCloudLab/policy-engine/schema/s3" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/util" "go.uber.org/zap" ) type PolicySettings interface { PolicyDenyByDefault() bool ACLEnabled() bool } type FrostFSIDInformer interface { GetUserGroupIDs(userHash util.Uint160) ([]string, error) } // BucketResolveFunc is a func to resolve bucket info by name. type BucketResolveFunc func(ctx context.Context, bucket string) (*data.BucketInfo, error) type PolicyConfig struct { Storage engine.ChainRouter FrostfsID FrostFSIDInformer Settings PolicySettings Domains []string Log *zap.Logger BucketResolver BucketResolveFunc } func PolicyCheck(cfg PolicyConfig) Func { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if err := policyCheck(r, cfg); err != nil { reqLogOrDefault(ctx, cfg.Log).Error(logs.PolicyValidationFailed, zap.Error(err)) if _, wrErr := WriteErrorResponse(w, GetReqInfo(ctx), err); wrErr != nil { reqLogOrDefault(ctx, cfg.Log).Error(logs.FailedToWriteResponse, zap.Error(wrErr)) } return } h.ServeHTTP(w, r) }) } } func policyCheck(r *http.Request, cfg PolicyConfig) error { reqType, bktName, objName := getBucketObject(r, cfg.Domains) req, err := getPolicyRequest(r, cfg.FrostfsID, reqType, bktName, objName, cfg.Log) if err != nil { return err } reqInfo := GetReqInfo(r.Context()) target := engine.NewRequestTargetWithNamespace(reqInfo.Namespace) st, found, err := cfg.Storage.IsAllowed(chain.S3, target, req) if err != nil { return err } if !found { st = chain.NoRuleFound } switch { case st == chain.Allow: return nil case st != chain.NoRuleFound: return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String())) } isAPE, err := isAPEBehavior(r.Context(), req, cfg, reqType, bktName) if err != nil { return err } if isAPE && cfg.Settings.PolicyDenyByDefault() { return apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String())) } return nil } func isAPEBehavior(ctx context.Context, req *testutil.Request, cfg PolicyConfig, reqType ReqType, bktName string) (bool, error) { if reqType == noneType || strings.HasSuffix(req.Operation(), CreateBucketOperation) { return !cfg.Settings.ACLEnabled(), nil } bktInfo, err := cfg.BucketResolver(ctx, bktName) // we cannot use reqInfo.BucketName because it hasn't been set yet if err != nil { return false, err } return bktInfo.APEEnabled, nil } func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, reqType ReqType, bktName string, objName string, log *zap.Logger) (*testutil.Request, error) { var ( owner string groups []string ) ctx := r.Context() bd, err := GetBoxData(ctx) if err == nil && bd.Gate.BearerToken != nil { pk, err := keys.NewPublicKeyFromBytes(bd.Gate.BearerToken.SigningKeyBytes(), elliptic.P256()) if err != nil { return nil, fmt.Errorf("parse pubclic key from btoken: %w", err) } owner = pk.Address() groups, err = frostfsid.GetUserGroupIDs(pk.GetScriptHash()) if err != nil { return nil, fmt.Errorf("get group ids: %w", err) } } op := determineOperation(r, reqType) var res string switch reqType { case objectType: res = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktName, objName) default: res = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName) } reqLogOrDefault(r.Context(), log).Debug(logs.PolicyRequest, zap.String("action", op), zap.String("resource", res), zap.String("owner", owner)) return testutil.NewRequest(op, testutil.NewResource(res, nil), map[string]string{ s3.PropertyKeyOwner: owner, common.PropertyKeyFrostFSIDGroupID: chain.FormCondSliceContainsValue(groups), }, ), nil } type ReqType int const ( noneType ReqType = iota bucketType objectType ) func getBucketObject(r *http.Request, domains []string) (reqType ReqType, bktName string, objName string) { for _, domain := range domains { ind := strings.Index(r.Host, "."+domain) if ind == -1 { continue } bkt := r.Host[:ind] if obj := strings.TrimPrefix(r.URL.Path, "/"); obj != "" { return objectType, bkt, obj } return bucketType, bkt, "" } bktObj := strings.TrimPrefix(r.URL.Path, "/") if bktObj == "" { return noneType, "", "" } if ind := strings.IndexByte(bktObj, '/'); ind != -1 { return objectType, bktObj[:ind], bktObj[ind+1:] } return bucketType, bktObj, "" } func determineOperation(r *http.Request, reqType ReqType) (operation string) { switch reqType { case objectType: operation = determineObjectOperation(r) case bucketType: operation = determineBucketOperation(r) default: operation = determineGeneralOperation(r) } return "s3:" + operation } func determineBucketOperation(r *http.Request) string { query := r.URL.Query() switch r.Method { case http.MethodOptions: return OptionsOperation case http.MethodHead: return HeadBucketOperation case http.MethodGet: switch { case query.Has(UploadsQuery): return ListMultipartUploadsOperation case query.Has(LocationQuery): return GetBucketLocationOperation case query.Has(PolicyQuery): return GetBucketPolicyOperation case query.Has(LifecycleQuery): return GetBucketLifecycleOperation case query.Has(EncryptionQuery): return GetBucketEncryptionOperation case query.Has(CorsQuery): return GetBucketCorsOperation case query.Has(ACLQuery): return GetBucketACLOperation case query.Has(WebsiteQuery): return GetBucketWebsiteOperation case query.Has(AccelerateQuery): return GetBucketAccelerateOperation case query.Has(RequestPaymentQuery): return GetBucketRequestPaymentOperation case query.Has(LoggingQuery): return GetBucketLoggingOperation case query.Has(ReplicationQuery): return GetBucketReplicationOperation case query.Has(TaggingQuery): return GetBucketTaggingOperation case query.Has(ObjectLockQuery): return GetBucketObjectLockConfigOperation case query.Has(VersioningQuery): return GetBucketVersioningOperation case query.Has(NotificationQuery): return GetBucketNotificationOperation case query.Has(EventsQuery): return ListenBucketNotificationOperation case query.Has(VersionsQuery): return ListBucketObjectVersionsOperation case query.Get(ListTypeQuery) == "2" && query.Get(MetadataQuery) == "true": return ListObjectsV2MOperation case query.Get(ListTypeQuery) == "2": return ListObjectsV2Operation default: return ListObjectsV1Operation } case http.MethodPut: switch { case query.Has(CorsQuery): return PutBucketCorsOperation case query.Has(ACLQuery): return PutBucketACLOperation case query.Has(LifecycleQuery): return PutBucketLifecycleOperation case query.Has(EncryptionQuery): return PutBucketEncryptionOperation case query.Has(PolicyQuery): return PutBucketPolicyOperation case query.Has(ObjectLockQuery): return PutBucketObjectLockConfigOperation case query.Has(TaggingQuery): return PutBucketTaggingOperation case query.Has(VersioningQuery): return PutBucketVersioningOperation case query.Has(NotificationQuery): return PutBucketNotificationOperation default: return CreateBucketOperation } case http.MethodPost: switch { case query.Has(DeleteQuery): return DeleteMultipleObjectsOperation default: return PostObjectOperation } case http.MethodDelete: switch { case query.Has(CorsQuery): return DeleteBucketCorsOperation case query.Has(WebsiteQuery): return DeleteBucketWebsiteOperation case query.Has(TaggingQuery): return DeleteBucketTaggingOperation case query.Has(PolicyQuery): return DeleteBucketPolicyOperation case query.Has(LifecycleQuery): return DeleteBucketLifecycleOperation case query.Has(EncryptionQuery): return DeleteBucketEncryptionOperation default: return DeleteBucketOperation } } return "UnmatchedBucketOperation" } func determineObjectOperation(r *http.Request) string { query := r.URL.Query() switch r.Method { case http.MethodHead: return HeadObjectOperation case http.MethodGet: switch { case query.Has(UploadIDQuery): return ListPartsOperation case query.Has(ACLQuery): return GetObjectACLOperation case query.Has(TaggingQuery): return GetObjectTaggingOperation case query.Has(RetentionQuery): return GetObjectRetentionOperation case query.Has(LegalQuery): return GetObjectLegalHoldOperation case query.Has(AttributesQuery): return GetObjectAttributesOperation default: return GetObjectOperation } case http.MethodPut: switch { case query.Has(PartNumberQuery) && query.Has(UploadIDQuery) && r.Header.Get("X-Amz-Copy-Source") != "": return UploadPartCopyOperation case query.Has(PartNumberQuery) && query.Has(UploadIDQuery): return UploadPartOperation case query.Has(ACLQuery): return PutObjectACLOperation case query.Has(TaggingQuery): return PutObjectTaggingOperation case r.Header.Get("X-Amz-Copy-Source") != "": return CopyObjectOperation case query.Has(RetentionQuery): return PutObjectRetentionOperation case query.Has(LegalHoldQuery): return PutObjectLegalHoldOperation default: return PutObjectOperation } case http.MethodPost: switch { case query.Has(UploadIDQuery): return CompleteMultipartUploadOperation case query.Has(UploadsQuery): return CreateMultipartUploadOperation default: return SelectObjectContentOperation } case http.MethodDelete: switch { case query.Has(UploadIDQuery): return AbortMultipartUploadOperation case query.Has(TaggingQuery): return DeleteObjectTaggingOperation default: return DeleteObjectOperation } } return "UnmatchedObjectOperation" } func determineGeneralOperation(r *http.Request) string { if r.Method == http.MethodGet { return ListBucketsOperation } return "UnmatchedOperation" }