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/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 { ResolveNamespaceAlias(ns string) string PolicyDenyByDefault() bool } type FrostFSIDInformer interface { GetUserGroupIDs(userHash util.Uint160) ([]string, error) } func PolicyCheck(storage engine.ChainRouter, frostfsid FrostFSIDInformer, 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, frostfsid, 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, frostfsid FrostFSIDInformer, settings PolicySettings, domains []string, r *http.Request) (chain.Status, error) { req, err := getPolicyRequest(r, frostfsid, domains) if err != nil { return 0, err } reqInfo := GetReqInfo(r.Context()) target := engine.NewRequestTargetWithNamespace(settings.ResolveNamespaceAlias(reqInfo.Namespace)) st, found, err := storage.IsAllowed(chain.S3, target, req) if err != nil { return 0, err } if !found { st = chain.NoRuleFound } return st, nil } func getPolicyRequest(r *http.Request, frostfsid FrostFSIDInformer, domains []string) (*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, res := determineOperationAndResource(r, domains) 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 determineOperationAndResource(r *http.Request, domains []string) (operation string, resource string) { var ( reqType ReqType matchDomain bool ) for _, domain := range domains { ind := strings.Index(r.Host, "."+domain) if ind == -1 { continue } matchDomain = true reqType = bucketType bkt := r.Host[:ind] if obj := strings.TrimPrefix(r.URL.Path, "/"); obj != "" { reqType = objectType resource = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bkt, obj) } else { resource = fmt.Sprintf(s3.ResourceFormatS3Bucket, bkt) } break } if !matchDomain { bktObj := strings.TrimPrefix(r.URL.Path, "/") if ind := strings.IndexByte(bktObj, '/'); ind == -1 { reqType = bucketType resource = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktObj) if bktObj == "" { reqType = noneType } } else { reqType = objectType resource = fmt.Sprintf(s3.ResourceFormatS3BucketObject, bktObj[:ind], bktObj[ind+1:]) } } switch reqType { case objectType: operation = determineObjectOperation(r) case bucketType: operation = determineBucketOperation(r) default: operation = determineGeneralOperation(r) } return "s3:" + 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 "" }