package middleware import ( "crypto/elliptic" "fmt" "net/http" "strings" apiErr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy" "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/s3" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "go.uber.org/zap" ) type PolicySettings interface { ResolveNamespaceAlias(ns string) string PolicyDenyByDefault() bool } func PolicyCheck(storage engine.ChainRouter, settings PolicySettings, domains []string, log *zap.Logger) Func { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() st, err := policyCheck(storage, settings, domains, r) if err == nil { if st != chain.Allow && (st != chain.NoRuleFound || settings.PolicyDenyByDefault()) { err = apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String())) } } if err != nil { reqLogOrDefault(ctx, log).Error(logs.PolicyValidationFailed, zap.Error(err)) WriteErrorResponse(w, GetReqInfo(ctx), err) return } h.ServeHTTP(w, r) }) } } func policyCheck(storage engine.ChainRouter, settings PolicySettings, domains []string, r *http.Request) (chain.Status, error) { req, err := getPolicyRequest(r, domains) if err != nil { return 0, err } reqInfo := GetReqInfo(r.Context()) target := engine.NewRequestTargetWithNamespace(settings.ResolveNamespaceAlias(reqInfo.Namespace)) st, found, err := storage.IsAllowed(policy.S3ChainName, target, req) if err != nil { return 0, err } if !found { st = chain.NoRuleFound } return st, nil } func getPolicyRequest(r *http.Request, domains []string) (*testutil.Request, error) { var owner 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() } op, res := determineOperationAndResource(r, domains) return testutil.NewRequest(op, testutil.NewResource(res, nil), map[string]string{s3.PropertyKeyOwner: owner}, ), nil } type ReqType int const ( noneType ReqType = iota bucketType objectType ) func determineOperationAndResource(r *http.Request, domains []string) (operation string, resource string) { reqType := noneType var matchDomain bool for _, domain := range domains { if ind := strings.Index(r.Host, "."+domain); ind != -1 { matchDomain = true reqType = bucketType resource = r.Host[:ind] trimmedObj := strings.TrimPrefix(r.URL.Path, "/") if trimmedObj != "" { reqType = objectType resource += "/" + trimmedObj } } } if !matchDomain { resource = strings.TrimPrefix(r.URL.Path, "/") if resource != "" { if arr := strings.Split(resource, "/"); len(arr) == 1 { reqType = bucketType } else { reqType = objectType } } } switch reqType { case objectType: operation = determineObjectOperation(r) case bucketType: operation = determineBucketOperation(r) default: operation = determineGeneralOperation(r) } return operation, resource } 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 "" } 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 "" } func determineGeneralOperation(r *http.Request) string { if r.Method == http.MethodGet { return ListBucketsOperation } return "" }