From 386c604285875e5522a155222f250a8ad8b3c96d Mon Sep 17 00:00:00 2001 From: "naveen.p" Date: Thu, 4 Sep 2025 23:23:47 +0530 Subject: [PATCH 1/7] feat: implement public share management endpoints with security fixes - Add public share check endpoint: GET /v1/marketplace/shareinfo/public/check/{allocation} - Add public share recipients endpoint: GET /v1/marketplace/shareinfo/public/recipients/{allocation} - Add public share recipient removal endpoint: DELETE /v1/marketplace/shareinfo/public/recipient/{allocation} - Add public share revoke endpoint: DELETE /v1/marketplace/shareinfo/public/{allocation} Security & Features: - Owner-scoped recipient queries (prevents cross-owner data access) - Allocation ownership verification for all operations - Support for both private and public share types - Auth ticket validation and signature verification - Comprehensive error handling and validation - Rate limiting and connection management Database Operations: - CheckPublicShareExists: Verify public share existence - GetPublicShareRecipients: Get all recipients (owner-scoped) - RemovePublicShareRecipient: Remove specific recipient - RevokePublicShare: Remove all recipients for a file API Endpoints: - GET /v1/marketplace/shareinfo/public/check/{allocation} - Check existence - GET /v1/marketplace/shareinfo/public/recipients/{allocation} - List recipients - DELETE /v1/marketplace/shareinfo/public/recipient/{allocation} - Remove recipient - DELETE /v1/marketplace/shareinfo/public/{allocation} - Revoke share Security Fix: Added owner_id filtering to prevent cross-owner data access --- .../0chain.net/blobbercore/handler/handler.go | 378 ++++++++++++++++++ .../blobbercore/reference/shareinfo.go | 74 ++++ 2 files changed, 452 insertions(+) diff --git a/code/go/0chain.net/blobbercore/handler/handler.go b/code/go/0chain.net/blobbercore/handler/handler.go index ccd9afb73..8b41cb9cf 100644 --- a/code/go/0chain.net/blobbercore/handler/handler.go +++ b/code/go/0chain.net/blobbercore/handler/handler.go @@ -257,6 +257,26 @@ func setupHandlers(s *mux.Router) { RateLimitByGeneralRL(common.ToJSONResponse(WithConnection(ListShare)))). Methods(http.MethodOptions, http.MethodGet) + // NEW: Revoke public share endpoint + s.HandleFunc("/v1/marketplace/shareinfo/public/{allocation}", + RateLimitByGeneralRL(common.ToJSONResponse(WithConnection(RevokePublicShare)))). + Methods(http.MethodOptions, http.MethodDelete) + + // NEW: Add specific recipient removal endpoint + s.HandleFunc("/v1/marketplace/shareinfo/public/recipient/{allocation}", + RateLimitByGeneralRL(common.ToJSONResponse(WithConnection(RemovePublicShareRecipient)))). + Methods(http.MethodOptions, http.MethodDelete) + + // NEW: Add check public share exists endpoint + s.HandleFunc("/v1/marketplace/shareinfo/public/check/{allocation}", + RateLimitByGeneralRL(common.ToJSONResponse(WithConnection(CheckPublicShareExists)))). + Methods(http.MethodOptions, http.MethodGet) + + // NEW: Add get public share recipients endpoint + s.HandleFunc("/v1/marketplace/shareinfo/public/recipients/{allocation}", + RateLimitByGeneralRL(common.ToJSONResponse(WithConnection(GetPublicShareRecipients)))). + Methods(http.MethodOptions, http.MethodGet) + // lightweight http handler without heavy postgres transaction to improve performance s.HandleFunc("/v1/writemarker/lock/{allocation}", @@ -1786,6 +1806,364 @@ func RevokeShare(ctx context.Context, r *http.Request) (interface{}, error) { return resp, nil } +func RevokePublicShare(ctx context.Context, r *http.Request) (interface{}, error) { + + ctx = setupHandlerContext(ctx, r) + + allocationID := ctx.Value(constants.ContextKeyAllocationID).(string) + allocationTx := ctx.Value(constants.ContextKeyAllocation).(string) + allocationObj, err := storageHandler.verifyAllocation(ctx, allocationID, allocationTx, true) + if err != nil { + return nil, common.NewError("invalid_parameters", "Invalid allocation ID passed."+err.Error()) + } + + sign := r.Header.Get(common.ClientSignatureHeader) + signV2 := r.Header.Get(common.ClientSignatureHeaderV2) + + valid, err := verifySignatureFromRequest(allocationTx, sign, signV2, allocationObj.OwnerPublicKey) + if !valid || err != nil { + return nil, common.NewError("invalid_signature", "Invalid signature") + } + + path, _ := common.GetField(r, "path") + if path == "" { + return nil, common.NewError("invalid_parameters", "Invalid file path") + } + + filePathHash := fileref.GetReferenceLookup(allocationID, path) + _, err = reference.GetLimitedRefFieldsByLookupHash(ctx, allocationID, filePathHash, []string{"id", "type"}) + if err != nil { + return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) + } + + clientID := ctx.Value(constants.ContextKeyClient).(string) + if clientID != allocationObj.OwnerID { + return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") + } + + err = reference.DeletePublicShareInfo(ctx, &reference.ShareInfo{ + FilePathHash: filePathHash, + }) + if errors.Is(err, gorm.ErrRecordNotFound) { + resp := map[string]interface{}{ + "status": http.StatusNotFound, + "message": "Path not found", + } + return resp, nil + } + + if err != nil { + return nil, err + } + + resp := map[string]interface{}{ + "status": http.StatusNoContent, + "message": "Path successfully removed from allocation", + } + return resp, nil +} + +// swagger:route DELETE /v1/marketplace/shareinfo/public/recipient/{allocation} RemovePublicShareRecipient +// Removes a specific recipient from a public share. +// Handle remove specific recipient from public share requests. +// +// parameters: +// +// +name: allocation +// description: TxHash of the allocation in question. +// in: path +// required: true +// type: string +// +name: X-App-Client-ID +// description: The ID/Wallet address of the client sending the request. +// in: header +// type: string +// required: true +// +name: X-App-Client-Key +// description: The key of the client sending the request. +// in: header +// type: string +// required: true +// +name: ALLOCATION-ID +// description: The ID of the allocation in question. +// in: header +// type: string +// required: true +// +name: X-App-Client-Signature +// description: Digital signature of the client used to verify the request. +// in: header +// type: string +// required: true +// +name: X-App-Client-Signature-V2 +// description: Digital signature of the client used to verify the request. Overrides X-App-Client-Signature if provided. +// in: header +// type: string +// +// +name: path +// +// description: Path of the file to be shared. +// in: query +// type: string +// required: true +// +// +name: recipient_client_id +// +// description: The ID of the client to remove from the public share. +// in: query +// type: string +// required: true +// +// responses: +// +// 200: +// 400: +func RemovePublicShareRecipient(ctx context.Context, r *http.Request) (interface{}, error) { + ctx = setupHandlerContext(ctx, r) + + allocationID := ctx.Value(constants.ContextKeyAllocationID).(string) + allocationTx := ctx.Value(constants.ContextKeyAllocation).(string) + allocationObj, err := storageHandler.verifyAllocation(ctx, allocationID, allocationTx, true) + if err != nil { + return nil, common.NewError("invalid_parameters", "Invalid allocation ID passed."+err.Error()) + } + + sign := r.Header.Get(common.ClientSignatureHeader) + signV2 := r.Header.Get(common.ClientSignatureHeaderV2) + + valid, err := verifySignatureFromRequest(allocationTx, sign, signV2, allocationObj.OwnerPublicKey) + if !valid || err != nil { + return nil, common.NewError("invalid_signature", "Invalid signature") + } + + path, _ := common.GetField(r, "path") + if path == "" { + return nil, common.NewError("invalid_parameters", "Invalid file path") + } + + recipientClientID, _ := common.GetField(r, "recipient_client_id") + if recipientClientID == "" { + return nil, common.NewError("invalid_parameters", "Invalid recipient client ID") + } + + filePathHash := fileref.GetReferenceLookup(allocationID, path) + _, err = reference.GetLimitedRefFieldsByLookupHash(ctx, allocationID, filePathHash, []string{"id", "type"}) + if err != nil { + return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) + } + + clientID := ctx.Value(constants.ContextKeyClient).(string) + if clientID != allocationObj.OwnerID { + return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") + } + + // Remove specific recipient from public share + err = reference.RemovePublicShareRecipient(ctx, &reference.ShareInfo{ + ClientID: recipientClientID, + FilePathHash: filePathHash, + }) + if errors.Is(err, gorm.ErrRecordNotFound) { + resp := map[string]interface{}{ + "status": http.StatusNotFound, + "message": "Public share recipient not found", + } + return resp, nil + } + + if err != nil { + return nil, err + } + + resp := map[string]interface{}{ + "status": http.StatusNoContent, + "message": "Public share recipient successfully removed", + } + return resp, nil +} + +// swagger:route GET /v1/marketplace/shareinfo/public/check/{allocation} CheckPublicShareExists +// Checks if a public share exists for a file. +// Handle check public share exists requests. +// +// parameters: +// +// +name: allocation +// description: TxHash of the allocation in question. +// in: path +// required: true +// type: string +// +name: X-App-Client-ID +// description: The ID/Wallet address of the client sending the request. +// in: header +// type: string +// required: true +// +name: X-App-Client-Key +// description: The key of the client sending the request. +// in: header +// type: string +// required: true +// +name: ALLOCATION-ID +// description: The ID of the allocation in question. +// in: header +// type: string +// required: true +// +name: X-App-Client-Signature +// description: Digital signature of the client used to verify the request. +// in: header +// type: string +// required: true +// +name: X-App-Client-Signature-V2 +// description: Digital signature of the client used to verify the request. Overrides X-App-Client-Signature if provided. +// in: header +// type: string +// +// +name: path +// +// description: Path of the file to check. +// in: query +// type: string +// required: true +// +// responses: +// +// 200: +// 400: +func CheckPublicShareExists(ctx context.Context, r *http.Request) (interface{}, error) { + ctx = setupHandlerContext(ctx, r) + + allocationID := ctx.Value(constants.ContextKeyAllocationID).(string) + allocationTx := ctx.Value(constants.ContextKeyAllocation).(string) + allocationObj, err := storageHandler.verifyAllocation(ctx, allocationID, allocationTx, true) + if err != nil { + return nil, common.NewError("invalid_parameters", "Invalid allocation ID passed."+err.Error()) + } + + sign := r.Header.Get(common.ClientSignatureHeader) + signV2 := r.Header.Get(common.ClientSignatureHeaderV2) + + valid, err := verifySignatureFromRequest(allocationTx, sign, signV2, allocationObj.OwnerPublicKey) + if !valid || err != nil { + return nil, common.NewError("invalid_signature", "Invalid signature") + } + + path, _ := common.GetField(r, "path") + if path == "" { + return nil, common.NewError("invalid_parameters", "Invalid file path") + } + + filePathHash := fileref.GetReferenceLookup(allocationID, path) + _, err = reference.GetLimitedRefFieldsByLookupHash(ctx, allocationID, filePathHash, []string{"id", "type"}) + if err != nil { + return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) + } + + clientID := ctx.Value(constants.ContextKeyClient).(string) + if clientID != allocationObj.OwnerID { + return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") + } + + // Check if public share exists + exists, err := reference.CheckPublicShareExists(ctx, filePathHash) + if err != nil { + return nil, err + } + + resp := map[string]interface{}{ + "exists": exists, + "path": path, + } + return resp, nil +} + +// swagger:route GET /v1/marketplace/shareinfo/public/recipients/{allocation} GetPublicShareRecipients +// Gets all recipients of a public share. +// Handle get public share recipients requests. +// +// parameters: +// +// +name: allocation +// description: TxHash of the allocation in question. +// in: path +// required: true +// type: string +// +name: X-App-Client-ID +// description: The ID/Wallet address of the client sending the request. +// in: header +// type: string +// required: true +// +name: X-App-Client-Key +// description: The key of the client sending the request. +// in: header +// type: string +// required: true +// +name: ALLOCATION-ID +// description: The ID of the allocation in question. +// in: header +// type: string +// required: true +// +name: X-App-Client-Signature +// description: Digital signature of the client used to verify the request. +// in: header +// type: string +// required: true +// +name: X-App-Client-Signature-V2 +// description: Digital signature of the client used to verify the request. Overrides X-App-Client-Signature if provided. +// in: header +// type: string +// +// +name: path +// +// description: Path of the file to get recipients for. +// in: query +// type: string +// required: true +// +// responses: +// +// 200: +// 400: +func GetPublicShareRecipients(ctx context.Context, r *http.Request) (interface{}, error) { + ctx = setupHandlerContext(ctx, r) + + allocationID := ctx.Value(constants.ContextKeyAllocationID).(string) + allocationTx := ctx.Value(constants.ContextKeyAllocation).(string) + allocationObj, err := storageHandler.verifyAllocation(ctx, allocationID, allocationTx, true) + if err != nil { + return nil, common.NewError("invalid_parameters", "Invalid allocation ID passed."+err.Error()) + } + + sign := r.Header.Get(common.ClientSignatureHeader) + signV2 := r.Header.Get(common.ClientSignatureHeaderV2) + + valid, err := verifySignatureFromRequest(allocationTx, sign, signV2, allocationObj.OwnerPublicKey) + if !valid || err != nil { + return nil, common.NewError("invalid_signature", "Invalid signature") + } + + path, _ := common.GetField(r, "path") + if path == "" { + return nil, common.NewError("invalid_parameters", "Invalid file path") + } + + filePathHash := fileref.GetReferenceLookup(allocationID, path) + _, err = reference.GetLimitedRefFieldsByLookupHash(ctx, allocationID, filePathHash, []string{"id", "type"}) + if err != nil { + return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) + } + + clientID := ctx.Value(constants.ContextKeyClient).(string) + if clientID != allocationObj.OwnerID { + return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") + } + + // Get all recipients of the public share + recipients, err := reference.GetPublicShareRecipients(ctx, clientID, filePathHash) + if err != nil { + return nil, err + } + + return recipients, nil +} + // swagger:route POST /v1/marketplace/shareinfo/{allocation} PostShareInfo // Share a file. // Handle share file requests from clients. Returns generic mapping. diff --git a/code/go/0chain.net/blobbercore/reference/shareinfo.go b/code/go/0chain.net/blobbercore/reference/shareinfo.go index 2737dc9fd..b1c0e2ce0 100644 --- a/code/go/0chain.net/blobbercore/reference/shareinfo.go +++ b/code/go/0chain.net/blobbercore/reference/shareinfo.go @@ -65,6 +65,80 @@ func DeleteShareInfo(ctx context.Context, shareInfo *ShareInfo) error { return nil } +// CheckPublicShareExists checks if a public share exists for a file +func CheckPublicShareExists(ctx context.Context, filePathHash string) (bool, error) { + db := datastore.GetStore().GetTransaction(ctx) + var count int64 + + err := db.Model(&ShareInfo{}). + Where("file_path_hash = ? AND revoked = ?", filePathHash, false). + Count(&count).Error + + if err != nil { + return false, err + } + + return count > 0, nil +} + +// GetPublicShareRecipients gets all recipients of a public share for a specific owner +func GetPublicShareRecipients(ctx context.Context, ownerID, filePathHash string) ([]ShareInfo, error) { + db := datastore.GetStore().GetTransaction(ctx) + var recipients []ShareInfo + + err := db.Model(&ShareInfo{}). + Where("owner_id = ? AND file_path_hash = ? AND revoked = ?", ownerID, filePathHash, false). + Find(&recipients).Error + + return recipients, err +} + +// RemovePublicShareRecipient removes a specific recipient from a public share +func RemovePublicShareRecipient(ctx context.Context, shareInfo *ShareInfo) error { + db := datastore.GetStore().GetTransaction(ctx) + + result := db.Model(&ShareInfo{}). + Where(&ShareInfo{ + ClientID: shareInfo.ClientID, + FilePathHash: shareInfo.FilePathHash, + Revoked: false, + }). + Updates(ShareInfo{ + Revoked: true, + }) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + +func DeletePublicShareInfo(ctx context.Context, shareInfo *ShareInfo) error { + db := datastore.GetStore().GetTransaction(ctx) + + result := db.Model(&ShareInfo{}). + Where(&ShareInfo{ + FilePathHash: shareInfo.FilePathHash, + Revoked: false, + }). + Updates(ShareInfo{ + Revoked: true, + }) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + func UpdateShareInfo(ctx context.Context, shareInfo *ShareInfo) error { db := datastore.GetStore().GetTransaction(ctx) From 69c639356bac0597af1749bb5f20f24d7f91da1c Mon Sep 17 00:00:00 2001 From: "naveen.p" Date: Fri, 5 Sep 2025 21:59:00 +0530 Subject: [PATCH 2/7] Updated query param 'recipientClientId' for the route /shareinfo/public/recipient/ (handler.go) --- code/go/0chain.net/blobbercore/handler/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/go/0chain.net/blobbercore/handler/handler.go b/code/go/0chain.net/blobbercore/handler/handler.go index 8b41cb9cf..475239380 100644 --- a/code/go/0chain.net/blobbercore/handler/handler.go +++ b/code/go/0chain.net/blobbercore/handler/handler.go @@ -1940,7 +1940,7 @@ func RemovePublicShareRecipient(ctx context.Context, r *http.Request) (interface return nil, common.NewError("invalid_parameters", "Invalid file path") } - recipientClientID, _ := common.GetField(r, "recipient_client_id") + recipientClientID, _ := common.GetField(r, "recipientClientId") if recipientClientID == "" { return nil, common.NewError("invalid_parameters", "Invalid recipient client ID") } From 8b4e77f8b40471d483653d1ad019e2c1425210d0 Mon Sep 17 00:00:00 2001 From: Rokibul Hasan Date: Fri, 2 Jan 2026 23:48:02 +0600 Subject: [PATCH 3/7] Update workflow --- .../build-&-publish-docker-image.yml | 19 ++++------ .../workflows/build-for-conductor-testing.yml | 30 +++------------ .github/workflows/tests.yml | 38 +++++++++---------- docker.local/bin/build.base.sh | 9 ++++- docker.local/bin/build.blobber.sh | 9 ++++- docker.local/bin/build.validator.sh | 9 ++++- 6 files changed, 55 insertions(+), 59 deletions(-) diff --git a/.github/workflows/build-&-publish-docker-image.yml b/.github/workflows/build-&-publish-docker-image.yml index 2405a359d..bb3d5298d 100644 --- a/.github/workflows/build-&-publish-docker-image.yml +++ b/.github/workflows/build-&-publish-docker-image.yml @@ -22,7 +22,7 @@ env: jobs: blobber: timeout-minutes: 30 - runs-on: [blobber-runner] + runs-on: [hetzner-build] steps: - name: Cleanup before restarting conductor tests. run: | @@ -52,9 +52,6 @@ jobs: with: fetch-depth: 0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - # - name: Login to Docker Hub # uses: docker/login-action@v1 # with: @@ -95,19 +92,19 @@ jobs: SHORT_SHA=$(echo ${{ env.SHA }} | head -c 8) export DOCKER_IMAGE_BASE="$BLOBBER_BUILD_BASE_REGISTRY:$TAG" export DOCKER_IMAGE_SWAGGER="${BLOBBER_REGISTRY}:swagger_test" - # export DOCKER_BUILD="buildx build --platform linux/amd64,linux/arm64 --push" - export DOCKER_BUILD="build --push" + export DOCKER_BUILD="buildx build --load" export DOCKER_IMAGE_BLOBBER="-t ${BLOBBER_REGISTRY}:${TAG}" export CONTEXT_NAME="$RUNNER_NAME" && (docker context inspect "$CONTEXT_NAME" >/dev/null 2>&1 || docker context create "$CONTEXT_NAME") docker buildx inspect "blobber-$RUNNER_NAME" || docker buildx create --name "blobber-$RUNNER_NAME" --driver-opt network=host --buildkitd-flags '--allow-insecure-entitlement security.insecure' "$CONTEXT_NAME" docker buildx use "blobber-$RUNNER_NAME" ./docker.local/bin/build.blobber.sh docker tag ${BLOBBER_REGISTRY}:${TAG} ${BLOBBER_REGISTRY}:${TAG}-${SHORT_SHA} + docker push ${BLOBBER_REGISTRY}:${TAG} docker push ${BLOBBER_REGISTRY}:${TAG}-${SHORT_SHA} validator: timeout-minutes: 30 - runs-on: [blobber-runner] + runs-on: [hetzner-build] steps: - name: Cleanup before restarting conductor tests. run: | @@ -143,8 +140,6 @@ jobs: with: fetch-depth: 0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 # - name: Get changed files using defaults # id: changed-files @@ -181,20 +176,20 @@ jobs: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }} SHORT_SHA=$(echo ${{ env.SHA }} | head -c 8) export DOCKER_IMAGE_BASE="$BLOBBER_BUILD_BASE_REGISTRY:$TAG" - # export DOCKER_BUILD="buildx build --platform linux/amd64,linux/arm64 --push" - export DOCKER_BUILD="build --push" + export DOCKER_BUILD="buildx build --load" export DOCKER_IMAGE_VALIDATOR="-t ${VALIDATOR_REGISTRY}:${TAG}" export CONTEXT_NAME="$RUNNER_NAME" && (docker context inspect "$CONTEXT_NAME" >/dev/null 2>&1 || docker context create "$CONTEXT_NAME") docker buildx inspect "validator-$RUNNER_NAME" || docker buildx create --name "validator-$RUNNER_NAME" --driver-opt network=host --buildkitd-flags '--allow-insecure-entitlement security.insecure' "$CONTEXT_NAME" docker buildx use "validator-$RUNNER_NAME" ./docker.local/bin/build.validator.sh docker tag ${VALIDATOR_REGISTRY}:${TAG} ${VALIDATOR_REGISTRY}:${TAG}-${SHORT_SHA} + docker push ${VALIDATOR_REGISTRY}:${TAG} docker push ${VALIDATOR_REGISTRY}:${TAG}-${SHORT_SHA} system-tests: if: github.event_name != 'workflow_dispatch' needs: [blobber, validator] - runs-on: [ tests-suite ] + runs-on: [ hetzner-testsuite ] steps: - name: "Get current PR" uses: jwalton/gh-find-current-pr@v1 diff --git a/.github/workflows/build-for-conductor-testing.yml b/.github/workflows/build-for-conductor-testing.yml index 5f09576d1..11b5e7a01 100644 --- a/.github/workflows/build-for-conductor-testing.yml +++ b/.github/workflows/build-for-conductor-testing.yml @@ -19,7 +19,7 @@ env: jobs: build_blobber_for_conductor_testing: - runs-on: [self-hosted, arc-runner] + runs-on: [hetzner-build] steps: - name: Cleanup before restarting conductor tests. run: | @@ -27,6 +27,7 @@ jobs: rm -rf * cd /tmp rm -rf ./* + - name: Set docker image tag run: | if [[ "${{github.ref}}" == refs/pull/* ]]; then @@ -39,26 +40,9 @@ jobs: echo "BRANCH=$([ -z '${{ github.event.pull_request.head.sha }}' ] && echo ${GITHUB_REF#refs/*/} || echo $GITHUB_HEAD_REF)" >> $GITHUB_ENV echo "SHA=$([ -z '${{ github.event.pull_request.head.sha }}' ] && echo $GITHUB_SHA || echo '${{ github.event.pull_request.head.sha }}')" >> $GITHUB_ENV - - name: Setup go - uses: actions/setup-go@v3 - with: - go-version: ^1.21 # The Go version to download (if necessary) and use. - - name: Clone blobber - uses: actions/checkout@v1 + uses: actions/checkout@v3 - - name: Set up Docker Buildx - run: | - sudo apt-get update -y - sudo apt-get install wget - sudo apt-get install ca-certificates curl gnupg lsb-release -y - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt-get update -y - sudo apt-get install docker-ce docker-ce-cli containerd.io -y - export DOCKER_CLI_EXPERIMENTAL=enabled - docker run --privileged --rm tonistiigi/binfmt --install all - docker context create blobber_buildx - name: Login to Docker Hub uses: docker/login-action@v1 with: @@ -68,9 +52,5 @@ jobs: - name: Testing blobber & validator builds for conductor testing run: | SHORT_SHA=$(echo ${{ env.SHA }} | head -c 8) - # export DOCKER_IMAGE_BASE="${BLOBBER_REGISTRY}:base" - # export DOCKER_IMAGE_SWAGGER="${BLOBBER_REGISTRY}:swagger_test" - # export DOCKER_BUILD="buildx build --platform linux/amd64,linux/arm64 --push" - # export DOCKER_IMAGE_BLOBBER="-t ${BLOBBER_REGISTRY}:${TAG} -t ${BLOBBER_REGISTRY}:${TAG}-${SHORT_SHA}" - docker buildx create --driver-opt network=host --use --buildkitd-flags '--allow-insecure-entitlement security.insecure' --use blobber_buildx - ./docker.local/bin/build.base.sh && ./docker.local/bin/build.blobber-integration-tests.sh + docker buildx create --name blobber_buildx --driver-opt network=host --buildkitd-flags '--allow-insecure-entitlement security.insecure' --use || docker buildx use blobber_buildx + ./docker.local/bin/build.base.sh && ./docker.local/bin/build.blobber-integration-tests.sh \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6ea9b8bbc..41f90ebec 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,21 +12,19 @@ on: jobs: linter: name: Lints - runs-on: [self-hosted,blobber-runner] + runs-on: [hetzner-build] + env: + HOME: /root + GOCACHE: /root/.cache/go-build + GOPATH: /root/go steps: - # - name: Setup go - # uses: actions/setup-go@v3 - # with: - # go-version: ^1.22 # The Go version to download (if necessary) and use. - - name: Clone blobber - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install Packages run: | sudo apt-get update - sudo apt-get -y install build-essential nghttp2 libnghttp2-dev libssl-dev - sudo apt-get install wget + sudo apt-get -y install build-essential nghttp2 libnghttp2-dev libssl-dev wget - name: Buf-lint if: success() @@ -35,7 +33,6 @@ jobs: - name: Build Base run: | ./docker.local/bin/build.base.sh - golangci-lint cache clean - name: Golangci-lint uses: golangci/golangci-lint-action@v3 @@ -46,20 +43,23 @@ jobs: skip-pkg-cache: true skip-build-cache: true - unit-tests: name: Unit Tests - runs-on: [self-hosted,arc-runner] + runs-on: [hetzner-testsuite] + env: + HOME: /root + GOCACHE: /root/.cache/go-build + GOPATH: /root/go steps: + - name: Clone blobber + uses: actions/checkout@v3 + - name: Setup go uses: actions/setup-go@v3 with: - go-version: ^1.22 # The Go version to download (if necessary) and use. - - - name: Clone blobber - uses: actions/checkout@v2 + go-version: ^1.22 - - name: Install Make Command + - name: Install Dependencies run: | sudo apt-get update sudo apt-get -y install build-essential nghttp2 libnghttp2-dev libssl-dev wget @@ -68,10 +68,10 @@ jobs: run: | cd $GITHUB_WORKSPACE/code/go/0chain.net/ CGO_ENABLED=1 go test -tags bn256 -race -coverprofile=coverage.txt -covermode=atomic ./... + - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: - # fail_ci_if_error: true files: ./code/go/0chain.net/coverage.txt flags: Unit-Tests - verbose: true + verbose: true \ No newline at end of file diff --git a/docker.local/bin/build.base.sh b/docker.local/bin/build.base.sh index 74284d404..02aba1dea 100755 --- a/docker.local/bin/build.base.sh +++ b/docker.local/bin/build.base.sh @@ -27,4 +27,11 @@ echo "2> download herumi" [ ! -f ./docker.local/bin/bls.tar.gz ] && wget -O ./docker.local/bin/bls.tar.gz https://github.com/herumi/bls/archive/refs/tags/v1.22.tar.gz echo "3> docker build" -DOCKER_BUILDKIT=1 docker $DOCKER_BUILD --progress=plain --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/base.Dockerfile . -t $DOCKER_IMAGE_BASE --network host +# Set flags based on build type +if [[ "$DOCKER_BUILD" == *"buildx"* ]]; then + BUILD_FLAGS="--progress=plain" +else + BUILD_FLAGS="" +fi + +docker $DOCKER_BUILD $BUILD_FLAGS --build-arg GIT_COMMIT=$GIT_COMMIT -f docker.local/base.Dockerfile . -t $DOCKER_IMAGE_BASE --network host \ No newline at end of file diff --git a/docker.local/bin/build.blobber.sh b/docker.local/bin/build.blobber.sh index b5fdd007c..b06335049 100755 --- a/docker.local/bin/build.blobber.sh +++ b/docker.local/bin/build.blobber.sh @@ -33,4 +33,11 @@ echo "" # docker.local/bin/test.swagger.sh echo "2> docker build blobber" -DOCKER_BUILDKIT=1 docker $DOCKER_BUILD --progress=plain --build-arg GIT_COMMIT=$GIT_COMMIT --build-arg DOCKER_IMAGE_BASE=$DOCKER_IMAGE_BASE -f docker.local/blobber.Dockerfile . $DOCKER_IMAGE_BLOBBER --network host +# Set flags based on build type +if [[ "$DOCKER_BUILD" == *"buildx"* ]]; then + BUILD_FLAGS="--progress=plain" +else + BUILD_FLAGS="" +fi + +docker $DOCKER_BUILD $BUILD_FLAGS --build-arg GIT_COMMIT=$GIT_COMMIT --build-arg DOCKER_IMAGE_BASE=$DOCKER_IMAGE_BASE -f docker.local/blobber.Dockerfile . $DOCKER_IMAGE_BLOBBER --network host \ No newline at end of file diff --git a/docker.local/bin/build.validator.sh b/docker.local/bin/build.validator.sh index e91b1b8c4..91af131e9 100755 --- a/docker.local/bin/build.validator.sh +++ b/docker.local/bin/build.validator.sh @@ -28,4 +28,11 @@ echo " DOCKER_IMAGE_VALIDATOR=$DOCKER_IMAGE_VALIDATOR" echo "" echo "2> docker build validator" -DOCKER_BUILDKIT=1 docker $DOCKER_BUILD --progress=plain --build-arg GIT_COMMIT=$GIT_COMMIT --build-arg DOCKER_IMAGE_BASE=$DOCKER_IMAGE_BASE -f docker.local/validator.Dockerfile . $DOCKER_IMAGE_VALIDATOR \ No newline at end of file +# Set flags based on build type +if [[ "$DOCKER_BUILD" == *"buildx"* ]]; then + BUILD_FLAGS="--progress=plain" +else + BUILD_FLAGS="" +fi + +docker $DOCKER_BUILD $BUILD_FLAGS --build-arg GIT_COMMIT=$GIT_COMMIT --build-arg DOCKER_IMAGE_BASE=$DOCKER_IMAGE_BASE -f docker.local/validator.Dockerfile . $DOCKER_IMAGE_VALIDATOR \ No newline at end of file From 33e25a363561700cac43c723081fbe9e696ff928 Mon Sep 17 00:00:00 2001 From: "naveen.p" Date: Sat, 3 Jan 2026 06:32:26 +0530 Subject: [PATCH 4/7] ignored the linter check for the unused functions and types --- code/go/0chain.net/blobber/node.go | 1 + code/go/0chain.net/blobber/settings.go | 5 ++++- code/go/0chain.net/blobbercore/zcn/query.go | 5 ++++- code/go/0chain.net/core/transaction/entity.go | 4 +++- code/go/0chain.net/core/transaction/nonce.go | 10 +++++++++- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/code/go/0chain.net/blobber/node.go b/code/go/0chain.net/blobber/node.go index 27e1d2091..aec6849e8 100644 --- a/code/go/0chain.net/blobber/node.go +++ b/code/go/0chain.net/blobber/node.go @@ -87,6 +87,7 @@ func readKeysFromAws() error { return nil } +//nolint:unused func readKeysFromString(keyFileRaw *string) error { publicKey, privateKey, _, _ = encryption.ReadKeys( bytes.NewBufferString(*keyFileRaw)) diff --git a/code/go/0chain.net/blobber/settings.go b/code/go/0chain.net/blobber/settings.go index 2b59c65ab..32b42513b 100644 --- a/code/go/0chain.net/blobber/settings.go +++ b/code/go/0chain.net/blobber/settings.go @@ -4,14 +4,16 @@ import ( "context" "encoding/json" "errors" - "github.com/0chain/gosdk/core/transaction" "strconv" "time" + "github.com/0chain/gosdk/core/transaction" + "github.com/0chain/blobber/code/go/0chain.net/blobbercore/config" "github.com/0chain/blobber/code/go/0chain.net/core/logging" ) +// nolint:unused type storageScCB struct { done chan struct{} cct int64 @@ -19,6 +21,7 @@ type storageScCB struct { err error } +// nolint:unused func (ssc *storageScCB) OnInfoAvailable(op int, status int, info string, errStr string) { defer func() { ssc.done <- struct{}{} diff --git a/code/go/0chain.net/blobbercore/zcn/query.go b/code/go/0chain.net/blobbercore/zcn/query.go index 4cb3e0ad2..9f04c071e 100644 --- a/code/go/0chain.net/blobbercore/zcn/query.go +++ b/code/go/0chain.net/blobbercore/zcn/query.go @@ -4,9 +4,10 @@ import ( "encoding/json" "errors" "fmt" - "github.com/0chain/gosdk/zboxcore/sdk" "sync" + "github.com/0chain/gosdk/zboxcore/sdk" + "github.com/0chain/gosdk/zcncore" ) @@ -26,12 +27,14 @@ func GetBlobber(blobberID string) (*sdk.Blobber, error) { } +// nolint:unused type getBlobberCallback struct { wg sync.WaitGroup Blobber *zcncore.Blobber Error error } +// nolint:unused func (cb *getBlobberCallback) OnInfoAvailable(op int, status int, info string, err string) { defer cb.wg.Done() if status != zcncore.StatusSuccess { diff --git a/code/go/0chain.net/core/transaction/entity.go b/code/go/0chain.net/core/transaction/entity.go index 0bc02005b..010685b93 100644 --- a/code/go/0chain.net/core/transaction/entity.go +++ b/code/go/0chain.net/core/transaction/entity.go @@ -8,7 +8,8 @@ import ( ) var ( - Last50Transactions []string + Last50Transactions []string + //nolint:unused last50TransactionsMutex sync.Mutex ) @@ -94,6 +95,7 @@ const ( const STORAGE_CONTRACT_ADDRESS = "6dba10422e368813802877a85039d3985d96760ed844092319743fb3a76712d7" +//nolint:unused func updateLast50Transactions(data string) { last50TransactionsMutex.Lock() defer last50TransactionsMutex.Unlock() diff --git a/code/go/0chain.net/core/transaction/nonce.go b/code/go/0chain.net/core/transaction/nonce.go index 5aa651aaf..3b75cdf3d 100644 --- a/code/go/0chain.net/core/transaction/nonce.go +++ b/code/go/0chain.net/core/transaction/nonce.go @@ -1,15 +1,17 @@ package transaction import ( - "github.com/0chain/gosdk/core/client" "sync" "time" + "github.com/0chain/gosdk/core/client" + "github.com/0chain/blobber/code/go/0chain.net/core/logging" "github.com/0chain/gosdk/zcncore" "go.uber.org/zap" ) +// nolint:unused var monitor = &nonceMonitor{ failed: map[int64]int64{}, used: map[int64]time.Time{}, @@ -25,6 +27,7 @@ type nonceMonitor struct { shouldRefreshFromBalance bool } +// nolint:unused func (m *nonceMonitor) getNextUnusedNonce() int64 { m.Lock() defer m.Unlock() @@ -43,6 +46,7 @@ func (m *nonceMonitor) getNextUnusedNonce() int64 { } } +// nolint:unused func (m *nonceMonitor) recordFailedNonce(nonce int64) { m.Lock() defer m.Unlock() @@ -63,6 +67,7 @@ func (m *nonceMonitor) recordFailedNonce(nonce int64) { } } +// nolint:unused func (m *nonceMonitor) recordSuccess(nonce int64) { m.Lock() defer m.Unlock() @@ -83,6 +88,7 @@ func (m *nonceMonitor) recordSuccess(nonce int64) { } } +// nolint:unused func (m *nonceMonitor) refreshFromBalance() { logging.Logger.Info("Refreshing nonce from balance.") @@ -95,12 +101,14 @@ func (m *nonceMonitor) refreshFromBalance() { m.used = make(map[int64]time.Time) } +// nolint:unused type getNonceCallBack struct { waitCh chan struct{} nonce int64 hasError bool } +// nolint:unused func (g *getNonceCallBack) OnNonceAvailable(status int, nonce int64, info string) { if status != zcncore.StatusSuccess { g.hasError = true From 01d9cb0b14b6c5eb76c93dd8a9a23997bfac79b8 Mon Sep 17 00:00:00 2001 From: "naveen.p" Date: Sat, 3 Jan 2026 06:40:15 +0530 Subject: [PATCH 5/7] ignored linter check for unused type nonceMonitor --- code/go/0chain.net/core/transaction/nonce.go | 1 + 1 file changed, 1 insertion(+) diff --git a/code/go/0chain.net/core/transaction/nonce.go b/code/go/0chain.net/core/transaction/nonce.go index 3b75cdf3d..bf1e27ff4 100644 --- a/code/go/0chain.net/core/transaction/nonce.go +++ b/code/go/0chain.net/core/transaction/nonce.go @@ -19,6 +19,7 @@ var monitor = &nonceMonitor{ shouldRefreshFromBalance: true, } +// nolint:unused type nonceMonitor struct { sync.Mutex failed map[int64]int64 From 27be9ad176119bdc1ce84caf5e9b172418e30af0 Mon Sep 17 00:00:00 2001 From: "naveen.p" Date: Sat, 7 Mar 2026 12:07:02 +0530 Subject: [PATCH 6/7] fix(shareinfo,handler): fix five security and correctness issues in public share handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B-C1 — DeletePublicShareInfo revokes private shares Add `AND client_id = ''` to the WHERE clause so revoking a public share does not also revoke private (targeted) shares on the same file path. Public shares are identified by client_id = '', set by the owner inside their cryptographically signed auth ticket. B-C2 — GORM zero-value silently drops revoked = false from WHERE GORM v2 ignores zero-value struct fields in Where(&ShareInfo{}). Since bool false is Go's zero value, the revoked = false predicate was omitted from the generated SQL, causing already-revoked rows to be re-matched. Replace struct-based WHERE conditions with explicit string predicates and use map[string]interface{} for Updates across DeleteShareInfo, RemovePublicShareRecipient, and DeletePublicShareInfo. B-H1 — Ownership check ordered after file-path DB lookup GetLimitedRefFieldsByLookupHash was called before the clientID ownership assertion in all five share handlers, leaking whether a file path exists to non-owners via different error messages. Move the ownership check to immediately after signature verification in RevokeShare, RevokePublicShare, RemovePublicShareRecipient, CheckPublicShareExists, and GetPublicShareRecipients. B-H2 — CheckPublicShareExists counts private shares Missing client_id = '' filter caused the function to return true whenever any share (public or private) existed for the path, producing false positives for owners who had only shared a file privately. B-H3 — GetPublicShareRecipients returns private recipients and key material Missing client_id = '' filter allowed private recipients to appear in the public listing. Additionally, the implicit SELECT * returned re_encryption_key and client_encryption_public_key — sensitive proxy re-encryption key material — over a public-facing endpoint. Add the client_id filter and restrict the SELECT to non-sensitive identity and timing columns only. --- .../0chain.net/blobbercore/handler/handler.go | 50 +++++++++---------- .../blobbercore/reference/shareinfo.go | 34 ++++--------- 2 files changed, 34 insertions(+), 50 deletions(-) diff --git a/code/go/0chain.net/blobbercore/handler/handler.go b/code/go/0chain.net/blobbercore/handler/handler.go index 475239380..c245b23b3 100644 --- a/code/go/0chain.net/blobbercore/handler/handler.go +++ b/code/go/0chain.net/blobbercore/handler/handler.go @@ -1767,6 +1767,11 @@ func RevokeShare(ctx context.Context, r *http.Request) (interface{}, error) { return nil, common.NewError("invalid_signature", "Invalid signature") } + clientID := ctx.Value(constants.ContextKeyClient).(string) + if clientID != allocationObj.OwnerID { + return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") + } + path, _ := common.GetField(r, "path") if path == "" { return nil, common.NewError("invalid_parameters", "Invalid file path") @@ -1778,11 +1783,6 @@ func RevokeShare(ctx context.Context, r *http.Request) (interface{}, error) { return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) } - clientID := ctx.Value(constants.ContextKeyClient).(string) - if clientID != allocationObj.OwnerID { - return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") - } - err = reference.DeleteShareInfo(ctx, &reference.ShareInfo{ ClientID: refereeClientID, FilePathHash: filePathHash, @@ -1825,6 +1825,11 @@ func RevokePublicShare(ctx context.Context, r *http.Request) (interface{}, error return nil, common.NewError("invalid_signature", "Invalid signature") } + clientID := ctx.Value(constants.ContextKeyClient).(string) + if clientID != allocationObj.OwnerID { + return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") + } + path, _ := common.GetField(r, "path") if path == "" { return nil, common.NewError("invalid_parameters", "Invalid file path") @@ -1836,11 +1841,6 @@ func RevokePublicShare(ctx context.Context, r *http.Request) (interface{}, error return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) } - clientID := ctx.Value(constants.ContextKeyClient).(string) - if clientID != allocationObj.OwnerID { - return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") - } - err = reference.DeletePublicShareInfo(ctx, &reference.ShareInfo{ FilePathHash: filePathHash, }) @@ -1935,6 +1935,11 @@ func RemovePublicShareRecipient(ctx context.Context, r *http.Request) (interface return nil, common.NewError("invalid_signature", "Invalid signature") } + clientID := ctx.Value(constants.ContextKeyClient).(string) + if clientID != allocationObj.OwnerID { + return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") + } + path, _ := common.GetField(r, "path") if path == "" { return nil, common.NewError("invalid_parameters", "Invalid file path") @@ -1951,11 +1956,6 @@ func RemovePublicShareRecipient(ctx context.Context, r *http.Request) (interface return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) } - clientID := ctx.Value(constants.ContextKeyClient).(string) - if clientID != allocationObj.OwnerID { - return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") - } - // Remove specific recipient from public share err = reference.RemovePublicShareRecipient(ctx, &reference.ShareInfo{ ClientID: recipientClientID, @@ -2045,6 +2045,11 @@ func CheckPublicShareExists(ctx context.Context, r *http.Request) (interface{}, return nil, common.NewError("invalid_signature", "Invalid signature") } + clientID := ctx.Value(constants.ContextKeyClient).(string) + if clientID != allocationObj.OwnerID { + return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") + } + path, _ := common.GetField(r, "path") if path == "" { return nil, common.NewError("invalid_parameters", "Invalid file path") @@ -2056,11 +2061,6 @@ func CheckPublicShareExists(ctx context.Context, r *http.Request) (interface{}, return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) } - clientID := ctx.Value(constants.ContextKeyClient).(string) - if clientID != allocationObj.OwnerID { - return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") - } - // Check if public share exists exists, err := reference.CheckPublicShareExists(ctx, filePathHash) if err != nil { @@ -2139,6 +2139,11 @@ func GetPublicShareRecipients(ctx context.Context, r *http.Request) (interface{} return nil, common.NewError("invalid_signature", "Invalid signature") } + clientID := ctx.Value(constants.ContextKeyClient).(string) + if clientID != allocationObj.OwnerID { + return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") + } + path, _ := common.GetField(r, "path") if path == "" { return nil, common.NewError("invalid_parameters", "Invalid file path") @@ -2150,11 +2155,6 @@ func GetPublicShareRecipients(ctx context.Context, r *http.Request) (interface{} return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) } - clientID := ctx.Value(constants.ContextKeyClient).(string) - if clientID != allocationObj.OwnerID { - return nil, common.NewError("invalid_operation", "Operation needs to be performed by the owner of the allocation") - } - // Get all recipients of the public share recipients, err := reference.GetPublicShareRecipients(ctx, clientID, filePathHash) if err != nil { diff --git a/code/go/0chain.net/blobbercore/reference/shareinfo.go b/code/go/0chain.net/blobbercore/reference/shareinfo.go index b1c0e2ce0..e84dfc40d 100644 --- a/code/go/0chain.net/blobbercore/reference/shareinfo.go +++ b/code/go/0chain.net/blobbercore/reference/shareinfo.go @@ -46,14 +46,8 @@ func DeleteShareInfo(ctx context.Context, shareInfo *ShareInfo) error { db := datastore.GetStore().GetTransaction(ctx) result := db.Model(&ShareInfo{}). - Where(&ShareInfo{ - ClientID: shareInfo.ClientID, - FilePathHash: shareInfo.FilePathHash, - Revoked: false, - }). - Updates(ShareInfo{ - Revoked: true, - }) + Where("client_id = ? AND file_path_hash = ? AND revoked = ?", shareInfo.ClientID, shareInfo.FilePathHash, false). + Updates(map[string]interface{}{"revoked": true}) if result.Error != nil { return result.Error @@ -71,7 +65,7 @@ func CheckPublicShareExists(ctx context.Context, filePathHash string) (bool, err var count int64 err := db.Model(&ShareInfo{}). - Where("file_path_hash = ? AND revoked = ?", filePathHash, false). + Where("file_path_hash = ? AND client_id = '' AND revoked = ?", filePathHash, false). Count(&count).Error if err != nil { @@ -87,7 +81,8 @@ func GetPublicShareRecipients(ctx context.Context, ownerID, filePathHash string) var recipients []ShareInfo err := db.Model(&ShareInfo{}). - Where("owner_id = ? AND file_path_hash = ? AND revoked = ?", ownerID, filePathHash, false). + Select("id", "owner_id", "client_id", "file_path_hash", "expiry_at", "available_at", "revoked"). + Where("owner_id = ? AND file_path_hash = ? AND client_id = '' AND revoked = ?", ownerID, filePathHash, false). Find(&recipients).Error return recipients, err @@ -98,14 +93,8 @@ func RemovePublicShareRecipient(ctx context.Context, shareInfo *ShareInfo) error db := datastore.GetStore().GetTransaction(ctx) result := db.Model(&ShareInfo{}). - Where(&ShareInfo{ - ClientID: shareInfo.ClientID, - FilePathHash: shareInfo.FilePathHash, - Revoked: false, - }). - Updates(ShareInfo{ - Revoked: true, - }) + Where("client_id = ? AND file_path_hash = ? AND revoked = ?", shareInfo.ClientID, shareInfo.FilePathHash, false). + Updates(map[string]interface{}{"revoked": true}) if result.Error != nil { return result.Error @@ -121,13 +110,8 @@ func DeletePublicShareInfo(ctx context.Context, shareInfo *ShareInfo) error { db := datastore.GetStore().GetTransaction(ctx) result := db.Model(&ShareInfo{}). - Where(&ShareInfo{ - FilePathHash: shareInfo.FilePathHash, - Revoked: false, - }). - Updates(ShareInfo{ - Revoked: true, - }) + Where("file_path_hash = ? AND client_id = '' AND revoked = ?", shareInfo.FilePathHash, false). + Updates(map[string]interface{}{"revoked": true}) if result.Error != nil { return result.Error From 2de28c307443f7b3e0497a84abb29060e45c8d45 Mon Sep 17 00:00:00 2001 From: "naveen.p" Date: Sun, 15 Mar 2026 14:23:46 +0530 Subject: [PATCH 7/7] feat(blobber): add share_type to allow separate public/private share revocation - Add share_type to ShareInfo (reference/shareinfo.go): - Constants ShareTypePrivate, ShareTypePublic. - ShareInfo.ShareType field; default "private" when empty. - GetShareInfoByType(ownerID, filePathHash, shareType) for type-scoped lookups. - GetAnyActiveShare(ownerID, filePathHash) for download/auth (any type). - AddShareInfo / DeleteShareInfo use share_type; DeleteShareInfo filters by owner, client, file_path_hash, share_type. - CheckPublicShareExists, GetPublicShareRecipients, RemovePublicShareRecipient, RevokePublicShare scoped by share_type where applicable. - Migration (goose/migrations/1742500000_share_type.sql): - Add column share_type VARCHAR(16) NOT NULL DEFAULT 'private' to marketplace_share_info. - Indexes: (owner_id, file_path_hash, share_type), (client_id, file_path_hash, share_type). - Handlers (handler/handler.go): - InsertShare: read share_type from form; upsert by (owner, client, file_path_hash, share_type). - RevokeShare: pass ShareTypePrivate for private revoke. - Public share endpoints (RevokePublicShare, RemovePublicShareRecipient, CheckPublicShareExists, GetPublicShareRecipients) use ShareTypePublic and owner scoping. - Download/auth (authticket.go, object_operation_handler.go, storage_handler.go): - Use GetAnyActiveShare so either a public or private share allows access; no special client_id semantics. Aligns blobber with 0box per-recipient model and enables revoking only public or only private when the same user has both. --- .../blobbercore/handler/authticket.go | 2 +- .../0chain.net/blobbercore/handler/handler.go | 18 +++- .../handler/object_operation_handler.go | 2 +- .../blobbercore/handler/storage_handler.go | 2 +- .../blobbercore/reference/shareinfo.go | 88 ++++++++++++++----- goose/migrations/1742500000_share_type.sql | 22 +++++ 6 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 goose/migrations/1742500000_share_type.sql diff --git a/code/go/0chain.net/blobbercore/handler/authticket.go b/code/go/0chain.net/blobbercore/handler/authticket.go index d4ede2a8b..8b26043e9 100644 --- a/code/go/0chain.net/blobbercore/handler/authticket.go +++ b/code/go/0chain.net/blobbercore/handler/authticket.go @@ -41,7 +41,7 @@ func verifyAuthTicket(ctx context.Context, authTokenString string, allocationObj } if verifyShare { - shareInfo, err := reference.GetShareInfo(ctx, authToken.ClientID, authToken.FilePathHash) + shareInfo, err := reference.GetAnyActiveShare(ctx, authToken.ClientID, authToken.FilePathHash) if err != nil || shareInfo == nil { return nil, common.NewError("invalid_share", "client does not have permission to get the file meta. share does not exist") } diff --git a/code/go/0chain.net/blobbercore/handler/handler.go b/code/go/0chain.net/blobbercore/handler/handler.go index c245b23b3..9b411c703 100644 --- a/code/go/0chain.net/blobbercore/handler/handler.go +++ b/code/go/0chain.net/blobbercore/handler/handler.go @@ -1784,8 +1784,10 @@ func RevokeShare(ctx context.Context, r *http.Request) (interface{}, error) { } err = reference.DeleteShareInfo(ctx, &reference.ShareInfo{ + OwnerID: clientID, ClientID: refereeClientID, FilePathHash: filePathHash, + ShareType: reference.ShareTypePrivate, }) if errors.Is(err, gorm.ErrRecordNotFound) { resp := map[string]interface{}{ @@ -1842,6 +1844,7 @@ func RevokePublicShare(ctx context.Context, r *http.Request) (interface{}, error } err = reference.DeletePublicShareInfo(ctx, &reference.ShareInfo{ + OwnerID: clientID, FilePathHash: filePathHash, }) if errors.Is(err, gorm.ErrRecordNotFound) { @@ -1958,6 +1961,7 @@ func RemovePublicShareRecipient(ctx context.Context, r *http.Request) (interface // Remove specific recipient from public share err = reference.RemovePublicShareRecipient(ctx, &reference.ShareInfo{ + OwnerID: clientID, ClientID: recipientClientID, FilePathHash: filePathHash, }) @@ -2061,8 +2065,8 @@ func CheckPublicShareExists(ctx context.Context, r *http.Request) (interface{}, return nil, common.NewError("invalid_parameters", "Invalid file path. "+err.Error()) } - // Check if public share exists - exists, err := reference.CheckPublicShareExists(ctx, filePathHash) + // Check if public share exists for this owner and file + exists, err := reference.CheckPublicShareExists(ctx, clientID, filePathHash) if err != nil { return nil, err } @@ -2286,8 +2290,16 @@ func InsertShare(ctx context.Context, r *http.Request) (interface{}, error) { ExpiryAt: common.ToTime(authTicket.Expiration).UTC(), AvailableAt: common.ToTime(availableAt).UTC(), } + shareType := r.FormValue("share_type") + if shareType == "" { + shareType = reference.ShareTypePrivate + } + if shareType != reference.ShareTypePublic && shareType != reference.ShareTypePrivate { + shareType = reference.ShareTypePrivate + } + shareInfo.ShareType = shareType - existingShare, _ := reference.GetShareInfo(ctx, authTicket.ClientID, authTicket.FilePathHash) + existingShare, _ := reference.GetShareInfoByType(ctx, authTicket.ClientID, authTicket.FilePathHash, shareType) if existingShare != nil && len(existingShare.OwnerID) > 0 { err = reference.UpdateShareInfo(ctx, &shareInfo) diff --git a/code/go/0chain.net/blobbercore/handler/object_operation_handler.go b/code/go/0chain.net/blobbercore/handler/object_operation_handler.go index 29afc3bc5..10374d9b5 100644 --- a/code/go/0chain.net/blobbercore/handler/object_operation_handler.go +++ b/code/go/0chain.net/blobbercore/handler/object_operation_handler.go @@ -419,7 +419,7 @@ func (fsh *StorageHandler) DownloadFile(ctx context.Context, r *http.Request) (i return nil, common.NewErrorf("invalid_authticket", "cannot verify auth ticket: %v", err) } - shareInfo, err = reference.GetShareInfo(ctx, authToken.ClientID, authToken.FilePathHash) + shareInfo, err = reference.GetAnyActiveShare(ctx, authToken.ClientID, authToken.FilePathHash) if err != nil || shareInfo == nil { return nil, common.NewError("invalid_share", "client does not have permission to download the file. share does not exist") } diff --git a/code/go/0chain.net/blobbercore/handler/storage_handler.go b/code/go/0chain.net/blobbercore/handler/storage_handler.go index 1b35d9187..1317198d8 100644 --- a/code/go/0chain.net/blobbercore/handler/storage_handler.go +++ b/code/go/0chain.net/blobbercore/handler/storage_handler.go @@ -991,7 +991,7 @@ func (fsh *StorageHandler) GetRefs(ctx context.Context, r *http.Request) (*blobb return nil, common.NewError("json_unmarshall_error", fmt.Sprintf("error parsing authticket: %v", authTokenStr)) } - shareInfo, err := reference.GetShareInfo(ctx, authToken.ClientID, authToken.FilePathHash) + shareInfo, err := reference.GetAnyActiveShare(ctx, authToken.ClientID, authToken.FilePathHash) if err != nil { return nil, fsh.convertGormError(err) } diff --git a/code/go/0chain.net/blobbercore/reference/shareinfo.go b/code/go/0chain.net/blobbercore/reference/shareinfo.go index e84dfc40d..0154a7e89 100644 --- a/code/go/0chain.net/blobbercore/reference/shareinfo.go +++ b/code/go/0chain.net/blobbercore/reference/shareinfo.go @@ -9,12 +9,19 @@ import ( "gorm.io/gorm" ) +// ShareType constants for marketplace_share_info. +const ( + ShareTypePrivate = "private" + ShareTypePublic = "public" +) + // swagger:model ShareInfo type ShareInfo struct { ID int `gorm:"column:id;primaryKey"` OwnerID string `gorm:"column:owner_id;size:64;not null;index:idx_marketplace_share_info_for_owner,priority:1" json:"owner_id,omitempty"` ClientID string `gorm:"column:client_id;size:64;not null;index:idx_marketplace_share_info_for_client,priority:1" json:"client_id"` FilePathHash string `gorm:"column:file_path_hash;size:64;not null;index:idx_marketplace_share_info_for_owner,priority:2;index:idx_marketplace_share_info_for_client,priority:2" json:"file_path_hash,omitempty"` + ShareType string `gorm:"column:share_type;size:16;not null;default:private;index:idx_marketplace_share_info_for_owner,priority:3;index:idx_marketplace_share_info_for_client,priority:3" json:"share_type,omitempty"` ReEncryptionKey string `gorm:"column:re_encryption_key;not null" json:"re_encryption_key,omitempty"` ClientEncryptionPublicKey string `gorm:"column:client_encryption_public_key;not null" json:"client_encryption_public_key,omitempty"` Revoked bool `gorm:"column:revoked;not null" json:"revoked"` @@ -29,6 +36,9 @@ func (ShareInfo) TableName() string { // add share if it already doesnot exist func AddShareInfo(ctx context.Context, shareInfo *ShareInfo) error { db := datastore.GetStore().GetTransaction(ctx) + if shareInfo.ShareType == "" { + shareInfo.ShareType = ShareTypePrivate + } return db.Model(&ShareInfo{}).Create(shareInfo).Error } @@ -44,9 +54,13 @@ func ListShareInfoClientID(ctx context.Context, ownerID string, limit common.Pag func DeleteShareInfo(ctx context.Context, shareInfo *ShareInfo) error { db := datastore.GetStore().GetTransaction(ctx) - + shareType := shareInfo.ShareType + if shareType == "" { + shareType = ShareTypePrivate + } result := db.Model(&ShareInfo{}). - Where("client_id = ? AND file_path_hash = ? AND revoked = ?", shareInfo.ClientID, shareInfo.FilePathHash, false). + Where("owner_id = ? AND client_id = ? AND file_path_hash = ? AND share_type = ? AND revoked = ?", + shareInfo.OwnerID, shareInfo.ClientID, shareInfo.FilePathHash, shareType, false). Updates(map[string]interface{}{"revoked": true}) if result.Error != nil { @@ -59,13 +73,13 @@ func DeleteShareInfo(ctx context.Context, shareInfo *ShareInfo) error { return nil } -// CheckPublicShareExists checks if a public share exists for a file -func CheckPublicShareExists(ctx context.Context, filePathHash string) (bool, error) { +// CheckPublicShareExists checks if any active public share exists for a file for a given owner. +func CheckPublicShareExists(ctx context.Context, ownerID, filePathHash string) (bool, error) { db := datastore.GetStore().GetTransaction(ctx) var count int64 err := db.Model(&ShareInfo{}). - Where("file_path_hash = ? AND client_id = '' AND revoked = ?", filePathHash, false). + Where("owner_id = ? AND file_path_hash = ? AND share_type = ? AND revoked = ?", ownerID, filePathHash, ShareTypePublic, false). Count(&count).Error if err != nil { @@ -75,25 +89,26 @@ func CheckPublicShareExists(ctx context.Context, filePathHash string) (bool, err return count > 0, nil } -// GetPublicShareRecipients gets all recipients of a public share for a specific owner +// GetPublicShareRecipients gets all active recipients of the public share for (owner, file). func GetPublicShareRecipients(ctx context.Context, ownerID, filePathHash string) ([]ShareInfo, error) { db := datastore.GetStore().GetTransaction(ctx) var recipients []ShareInfo err := db.Model(&ShareInfo{}). - Select("id", "owner_id", "client_id", "file_path_hash", "expiry_at", "available_at", "revoked"). - Where("owner_id = ? AND file_path_hash = ? AND client_id = '' AND revoked = ?", ownerID, filePathHash, false). + Select("id", "owner_id", "client_id", "file_path_hash", "share_type", "expiry_at", "available_at", "revoked"). + Where("owner_id = ? AND file_path_hash = ? AND share_type = ? AND revoked = ?", ownerID, filePathHash, ShareTypePublic, false). Find(&recipients).Error return recipients, err } -// RemovePublicShareRecipient removes a specific recipient from a public share +// RemovePublicShareRecipient revokes the public-share entry for (owner, file, recipient). func RemovePublicShareRecipient(ctx context.Context, shareInfo *ShareInfo) error { db := datastore.GetStore().GetTransaction(ctx) result := db.Model(&ShareInfo{}). - Where("client_id = ? AND file_path_hash = ? AND revoked = ?", shareInfo.ClientID, shareInfo.FilePathHash, false). + Where("owner_id = ? AND client_id = ? AND file_path_hash = ? AND share_type = ? AND revoked = ?", + shareInfo.OwnerID, shareInfo.ClientID, shareInfo.FilePathHash, ShareTypePublic, false). Updates(map[string]interface{}{"revoked": true}) if result.Error != nil { @@ -106,11 +121,13 @@ func RemovePublicShareRecipient(ctx context.Context, shareInfo *ShareInfo) error return nil } +// DeletePublicShareInfo revokes all public-share entries for (owner, file). func DeletePublicShareInfo(ctx context.Context, shareInfo *ShareInfo) error { db := datastore.GetStore().GetTransaction(ctx) result := db.Model(&ShareInfo{}). - Where("file_path_hash = ? AND client_id = '' AND revoked = ?", shareInfo.FilePathHash, false). + Where("owner_id = ? AND file_path_hash = ? AND share_type = ? AND revoked = ?", + shareInfo.OwnerID, shareInfo.FilePathHash, ShareTypePublic, false). Updates(map[string]interface{}{"revoked": true}) if result.Error != nil { @@ -125,25 +142,56 @@ func DeletePublicShareInfo(ctx context.Context, shareInfo *ShareInfo) error { func UpdateShareInfo(ctx context.Context, shareInfo *ShareInfo) error { db := datastore.GetStore().GetTransaction(ctx) - + shareType := shareInfo.ShareType + if shareType == "" { + shareType = ShareTypePrivate + } return db.Model(&ShareInfo{}). - Where(&ShareInfo{ - ClientID: shareInfo.ClientID, - FilePathHash: shareInfo.FilePathHash, - }). + Where("owner_id = ? AND client_id = ? AND file_path_hash = ? AND share_type = ?", + shareInfo.OwnerID, shareInfo.ClientID, shareInfo.FilePathHash, shareType). Select("Revoked", "ReEncryptionKey", "ExpiryAt", "AvailableAt", "ClientEncryptionPublicKey"). Updates(shareInfo). Error } +// GetShareInfoByType returns the share row for (clientID, filePathHash, shareType) if any. +func GetShareInfoByType(ctx context.Context, clientID, filePathHash, shareType string) (*ShareInfo, error) { + db := datastore.GetStore().GetTransaction(ctx) + if shareType == "" { + shareType = ShareTypePrivate + } + shareInfo := &ShareInfo{} + err := db.Model(&ShareInfo{}). + Where("client_id = ? AND file_path_hash = ? AND share_type = ?", clientID, filePathHash, shareType). + Take(shareInfo).Error + + if err != nil { + return nil, err + } + return shareInfo, nil +} + +// GetAnyActiveShare returns any non-revoked share for (clientID, filePathHash). +// Used for download: access is allowed if the user has any valid share (public or private). +func GetAnyActiveShare(ctx context.Context, clientID, filePathHash string) (*ShareInfo, error) { + db := datastore.GetStore().GetTransaction(ctx) + shareInfo := &ShareInfo{} + err := db.Model(&ShareInfo{}). + Where("client_id = ? AND file_path_hash = ? AND revoked = ?", clientID, filePathHash, false). + First(shareInfo).Error + + if err != nil { + return nil, err + } + return shareInfo, nil +} + +// GetShareInfo returns the first share row for (clientID, filePathHash). Prefer GetShareInfoByType or GetAnyActiveShare. func GetShareInfo(ctx context.Context, clientID, filePathHash string) (*ShareInfo, error) { db := datastore.GetStore().GetTransaction(ctx) shareInfo := &ShareInfo{} err := db.Model(&ShareInfo{}). - Where(&ShareInfo{ - ClientID: clientID, - FilePathHash: filePathHash, - }). + Where("client_id = ? AND file_path_hash = ?", clientID, filePathHash). Take(shareInfo).Error if err != nil { diff --git a/goose/migrations/1742500000_share_type.sql b/goose/migrations/1742500000_share_type.sql new file mode 100644 index 000000000..466b78716 --- /dev/null +++ b/goose/migrations/1742500000_share_type.sql @@ -0,0 +1,22 @@ +-- +goose Up +-- +goose StatementBegin + +ALTER TABLE marketplace_share_info +ADD COLUMN IF NOT EXISTS share_type character varying(16) NOT NULL DEFAULT 'private'; + +CREATE INDEX IF NOT EXISTS idx_marketplace_share_info_owner_file_type +ON marketplace_share_info (owner_id, file_path_hash, share_type); + +CREATE INDEX IF NOT EXISTS idx_marketplace_share_info_client_file_type +ON marketplace_share_info (client_id, file_path_hash, share_type); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +DROP INDEX IF EXISTS idx_marketplace_share_info_client_file_type; +DROP INDEX IF EXISTS idx_marketplace_share_info_owner_file_type; +ALTER TABLE marketplace_share_info DROP COLUMN IF EXISTS share_type; + +-- +goose StatementEnd