diff --git a/ocean_provider/routes/consume.py b/ocean_provider/routes/consume.py index e2596e6f..91b6860a 100644 --- a/ocean_provider/routes/consume.py +++ b/ocean_provider/routes/consume.py @@ -336,6 +336,11 @@ def download(): # grab asset for did from the metadatastore associated with # the datatoken address asset = get_asset_from_metadatastore(get_metadata_url(), did) + + consumable, message = check_asset_consumable(asset, consumer_address, logger) + if not consumable: + return error_response(message, 400, logger) + service = asset.get_service_by_id(service_id) if service.type != ServiceType.ACCESS: diff --git a/ocean_provider/utils/asset.py b/ocean_provider/utils/asset.py index af8bf4eb..f01fc8cd 100644 --- a/ocean_provider/utils/asset.py +++ b/ocean_provider/utils/asset.py @@ -100,10 +100,11 @@ def get_asset_from_metadatastore(metadata_url, document_id) -> Optional[Asset]: def check_asset_consumable(asset, consumer_address, logger, custom_url=None): if not asset.nft or "address" not in asset.nft or not asset.chain_id: return False, "Asset malformed or disabled." + web3 = get_web3(asset.chain_id) nft_contract = get_data_nft_contract(web3, asset.nft["address"]) - if nft_contract.caller.getMetaData()[2] not in [0, 5]: + if nft_contract.functions.getMetaData().call()[2] not in [0, 5]: return False, "Asset is not consumable." code = asset.is_consumable({"type": "address", "value": consumer_address}) diff --git a/ocean_provider/utils/credentials.py b/ocean_provider/utils/credentials.py index 58ac5a27..9ecb06d3 100644 --- a/ocean_provider/utils/credentials.py +++ b/ocean_provider/utils/credentials.py @@ -2,6 +2,7 @@ # Copyright 2023 Ocean Protocol Foundation # SPDX-License-Identifier: Apache-2.0 # +import json from typing import Optional from ocean_provider.utils.consumable import ConsumableCodes, MalformedCredential @@ -100,7 +101,12 @@ def get_address_entry_of_class(self, access_class: str = "allow") -> Optional[di """Get address credentials entry of the specified access class. access_class = "allow" or "deny".""" if not self.asset.credentials: return None - entries = self.asset.credentials.get(access_class, []) + if isinstance(self.asset.credentials, str): + credentials = json.loads(self.asset.credentials) + else: + credentials = self.asset.credentials + + entries = credentials.get(access_class, []) address_entries = [entry for entry in entries if entry.get("type") == "address"] return address_entries[0] if address_entries else None diff --git a/ocean_provider/utils/data_nft.py b/ocean_provider/utils/data_nft.py index 80d5432b..ee587ba3 100644 --- a/ocean_provider/utils/data_nft.py +++ b/ocean_provider/utils/data_nft.py @@ -42,7 +42,7 @@ def get_data_nft_contract(web3: Web3, address: Optional[str] = None) -> Contract especially the `getMetaData` contract method. """ abi = get_contract_definition("ERC721Template")["abi"] - return web3.eth.contract(address=address, abi=abi) + return web3.eth.contract(address=web3.toChecksumAddress(address), abi=abi) def get_metadata(web3: Web3, address: str) -> Tuple[str, str, MetadataState, bool]: diff --git a/ocean_provider/validation/algo.py b/ocean_provider/validation/algo.py index b3d870ad..fb570f8e 100644 --- a/ocean_provider/validation/algo.py +++ b/ocean_provider/validation/algo.py @@ -14,6 +14,8 @@ get_asset_from_metadatastore, ) from ocean_provider.utils.basics import get_metadata_url, get_provider_wallet, get_web3 +from ocean_provider.utils.consumable import ConsumableCodes +from ocean_provider.utils.credentials import AddressCredential from ocean_provider.utils.datatoken import ( record_consume_request, validate_order, @@ -308,6 +310,15 @@ def preliminary_algo_validation(self): self.message = "file_unavailable" return False + consumable, message = check_asset_consumable( + algo_ddo, self.consumer_address, logger, service.service_endpoint + ) + + if not consumable: + self.resource = "algorithm.credentials" + self.message = message + return False + return True diff --git a/ocean_provider/validation/test/test_algo_validation.py b/ocean_provider/validation/test/test_algo_validation.py index e94f9555..b202a91b 100644 --- a/ocean_provider/validation/test/test_algo_validation.py +++ b/ocean_provider/validation/test/test_algo_validation.py @@ -2,16 +2,23 @@ # Copyright 2023 Ocean Protocol Foundation # SPDX-License-Identifier: Apache-2.0 # +import os import copy from unittest.mock import Mock, patch import pytest +from eth_account import Account from ocean_provider.utils.asset import Asset +from ocean_provider.utils.currency import to_wei from ocean_provider.utils.services import Service, ServiceType from ocean_provider.validation.algo import WorkflowValidator from tests.ddo.ddo_sample1_compute import alg_ddo_dict, ddo_dict -from tests.helpers.compute_helpers import get_future_valid_until -from tests.test_helpers import get_first_service_by_type +from tests.test_helpers import get_first_service_by_type, get_ocean_token_address +from ocean_provider.utils.datatoken import get_datatoken_contract +from tests.helpers.compute_helpers import ( + get_future_valid_until, + build_and_send_ddo_with_compute_service, +) provider_fees_event = Mock() provider_fees_event.args.providerData = {"environment": "ocean-compute"} @@ -1010,3 +1017,66 @@ def side_effect(*args, **kwargs): assert validator.validate() is False assert validator.resource == "algorithm" assert validator.message == "file_unavailable" + + +@pytest.mark.unit +def test_algo_credentials( + client, + provider_address, + consumer_address, + publisher_wallet, + web3, + consumer_wallet, + free_c2d_env, +): + valid_until = get_future_valid_until() + deployer_wallet = Account.from_key(os.getenv("FACTORY_DEPLOYER_PRIVATE_KEY")) + fee_token = get_datatoken_contract(web3, get_ocean_token_address(web3)) + fee_token.functions.mint(consumer_wallet.address, to_wei(80)).transact( + {"from": deployer_wallet.address} + ) + custom_algo_credentials = { + "allow": [], + "deny": [{"type": "address", "values": [consumer_address]}], + } + ddo, tx_id, alg_ddo, alg_tx_id = build_and_send_ddo_with_compute_service( + client, + publisher_wallet, + consumer_wallet, + True, + custom_algo_credentials=custom_algo_credentials, + c2d_address=free_c2d_env["consumerAddress"], + valid_until=valid_until, + c2d_environment=free_c2d_env["id"], + fee_token_args=(fee_token, to_wei(80)), + ) + sa = get_first_service_by_type(ddo, ServiceType.COMPUTE) + sa_compute = get_first_service_by_type(alg_ddo, ServiceType.ACCESS) + + data = { + "dataset": {"documentId": ddo.did, "serviceId": sa.id, "transferTxId": tx_id}, + "algorithm": { + "documentId": alg_ddo.did, + "serviceId": sa_compute.id, + "transferTxId": alg_tx_id, + }, + } + + def side_effect(*args, **kwargs): + nonlocal ddo, alg_ddo + if ddo.did == args[1]: + return ddo + if alg_ddo.did == args[1]: + return alg_ddo + + with patch( + "ocean_provider.validation.algo.get_asset_from_metadatastore", + side_effect=side_effect, + ): + validator = WorkflowValidator(consumer_address, data) + assert validator.validate() is False + assert validator.resource == "algorithm.credentials" + assert ( + validator.message + == f"Error: Access to asset {alg_ddo.did} was denied with code: ConsumableCodes.CREDENTIAL_IN_DENY_LIST." + ) diff --git a/tests/helpers/compute_helpers.py b/tests/helpers/compute_helpers.py index 119a8cab..3161b946 100644 --- a/tests/helpers/compute_helpers.py +++ b/tests/helpers/compute_helpers.py @@ -25,6 +25,7 @@ def build_and_send_ddo_with_compute_service( publisher_wallet, consumer_wallet, alg_diff=False, + custom_algo_credentials=None, asset_type=None, c2d_address=None, do_send=True, @@ -40,15 +41,27 @@ def build_and_send_ddo_with_compute_service( if c2d_address is None: c2d_address = consumer_wallet.address if alg_diff: - alg_ddo = get_registered_asset( - publisher_wallet, - custom_metadata=algo_metadata, - custom_service_endpoint="http://172.15.0.7:8030", - timeout=timeout, - unencrypted_files_list=[ - {"url": this_is_a_gist, "type": "url", "method": "GET"} - ], - ) + if custom_algo_credentials: + alg_ddo = get_registered_asset( + publisher_wallet, + custom_metadata=algo_metadata, + custom_service_endpoint="http://172.15.0.7:8030", + custom_credentials=custom_algo_credentials, + timeout=timeout, + unencrypted_files_list=[ + {"url": this_is_a_gist, "type": "url", "method": "GET"} + ], + ) + else: + alg_ddo = get_registered_asset( + publisher_wallet, + custom_metadata=algo_metadata, + custom_service_endpoint="http://172.15.0.7:8030", + timeout=timeout, + unencrypted_files_list=[ + {"url": this_is_a_gist, "type": "url", "method": "GET"} + ], + ) else: alg_ddo = get_registered_asset( publisher_wallet, @@ -58,7 +71,6 @@ def build_and_send_ddo_with_compute_service( {"url": this_is_a_gist, "type": "url", "method": "GET"} ], ) - # publish an algorithm asset (asset with metadata of type `algorithm`) service = get_first_service_by_type(alg_ddo, ServiceType.ACCESS) mint_100_datatokens( diff --git a/tests/test_download.py b/tests/test_download.py index 82708638..e5513ffe 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -4,15 +4,23 @@ # import copy import logging +import os import time from unittest.mock import patch import pytest +from eth_account import Account from ocean_provider.constants import BaseURLs from ocean_provider.utils.accounts import sign_message +from ocean_provider.utils.currency import to_wei from ocean_provider.utils.data_nft_factory import get_data_nft_factory_address +from ocean_provider.utils.datatoken import get_datatoken_contract from ocean_provider.utils.provider_fees import get_provider_fees from ocean_provider.utils.services import ServiceType +from tests.helpers.compute_helpers import ( + get_future_valid_until, + build_and_send_ddo_with_compute_service, +) from tests.helpers.constants import ARWEAVE_TRANSACTION_ID from tests.helpers.nonce import build_nonce from tests.test_auth import create_token @@ -27,6 +35,7 @@ start_multiple_order, start_order, try_download, + get_ocean_token_address, ) logger = logging.getLogger(__name__) @@ -454,3 +463,150 @@ def test_download_arweave(client, publisher_wallet, consumer_wallet, web3): response.data.decode("utf-8").partition("\n")[0] == "% 1. Title: Branin Function" ) + + +@pytest.mark.integration +def test_consume_algo_with_credentials( + client, publisher_wallet, consumer_wallet, free_c2d_env, web3 +): + valid_until = get_future_valid_until() + deployer_wallet = Account.from_key(os.getenv("FACTORY_DEPLOYER_PRIVATE_KEY")) + fee_token = get_datatoken_contract(web3, get_ocean_token_address(web3)) + fee_token.functions.mint(consumer_wallet.address, to_wei(80)).transact( + {"from": deployer_wallet.address} + ) + # Use case 1: Consume an algorithm asset by an address which is in the denied list + algo_credentials = { + "allow": [], + "deny": [{"type": "address", "values": [consumer_wallet.address]}], + } + + ddo, tx_id, alg_ddo, alg_tx_id = build_and_send_ddo_with_compute_service( + client, + publisher_wallet, + consumer_wallet, + True, + custom_algo_credentials=algo_credentials, + c2d_address=free_c2d_env["consumerAddress"], + valid_until=valid_until, + c2d_environment=free_c2d_env["id"], + fee_token_args=(fee_token, to_wei(80)), + ) + sa_compute = get_first_service_by_type(alg_ddo, ServiceType.ACCESS) + mint_100_datatokens( + web3, sa_compute.datatoken_address, consumer_wallet.address, publisher_wallet + ) + tx_id, _ = start_order( + web3, + sa_compute.datatoken_address, + consumer_wallet.address, + sa_compute.index, + get_provider_fees(alg_ddo, sa_compute, consumer_wallet.address, 0), + consumer_wallet, + ) + + payload = { + "documentId": alg_ddo.did, + "serviceId": sa_compute.id, + "consumerAddress": consumer_wallet.address, + "transferTxId": tx_id, + "fileIndex": 0, + } + + download_endpoint = BaseURLs.SERVICES_URL + "/download" + nonce = build_nonce(consumer_wallet.address) + _msg = f"{alg_ddo.did}{nonce}" + payload["signature"] = sign_message(_msg, consumer_wallet) + payload["nonce"] = nonce + response = client.get( + sa_compute.service_endpoint + download_endpoint, query_string=payload + ) + assert response.status_code == 400, f"{response.data}" + assert ( + response.json["error"] + == f"Error: Access to asset {alg_ddo.did} was denied with code: ConsumableCodes.CREDENTIAL_IN_DENY_LIST." + ) + + # Use case 2: Consume an algorithm asset by an address which is not in the allowed list + + algo_credentials = { + "allow": [{"type": "address", "values": [consumer_wallet.address]}], + "deny": [], + } + + ddo, tx_id, alg_ddo, alg_tx_id = build_and_send_ddo_with_compute_service( + client, + publisher_wallet, + deployer_wallet, + True, + custom_algo_credentials=algo_credentials, + c2d_address=free_c2d_env["consumerAddress"], + valid_until=valid_until, + c2d_environment=free_c2d_env["id"], + fee_token_args=(fee_token, to_wei(80)), + ) + sa_compute = get_first_service_by_type(alg_ddo, ServiceType.ACCESS) + mint_100_datatokens( + web3, sa_compute.datatoken_address, deployer_wallet.address, publisher_wallet + ) + tx_id, _ = start_order( + web3, + sa_compute.datatoken_address, + deployer_wallet.address, + sa_compute.index, + get_provider_fees(alg_ddo, sa_compute, deployer_wallet.address, 0), + deployer_wallet, + ) + + payload = { + "documentId": alg_ddo.did, + "serviceId": sa_compute.id, + "consumerAddress": deployer_wallet.address, + "transferTxId": tx_id, + "fileIndex": 0, + } + + download_endpoint = BaseURLs.SERVICES_URL + "/download" + nonce = build_nonce(deployer_wallet.address) + _msg = f"{alg_ddo.did}{nonce}" + payload["signature"] = sign_message(_msg, deployer_wallet) + payload["nonce"] = nonce + response = client.get( + sa_compute.service_endpoint + download_endpoint, query_string=payload + ) + assert response.status_code == 400, f"{response.data}" + assert ( + response.json["error"] + == f"Error: Access to asset {alg_ddo.did} was denied with code: ConsumableCodes.CREDENTIAL_NOT_IN_ALLOW_LIST." + ) + + # Use case 3: Consume asset by allowed address + mint_100_datatokens( + web3, sa_compute.datatoken_address, consumer_wallet.address, publisher_wallet + ) + tx_id, _ = start_order( + web3, + sa_compute.datatoken_address, + consumer_wallet.address, + sa_compute.index, + get_provider_fees(alg_ddo, sa_compute, consumer_wallet.address, 0), + consumer_wallet, + ) + + payload = { + "documentId": alg_ddo.did, + "serviceId": sa_compute.id, + "consumerAddress": consumer_wallet.address, + "transferTxId": tx_id, + "fileIndex": 0, + } + + download_endpoint = BaseURLs.SERVICES_URL + "/download" + nonce = build_nonce(consumer_wallet.address) + _msg = f"{alg_ddo.did}{nonce}" + payload["signature"] = sign_message(_msg, consumer_wallet) + payload["nonce"] = nonce + response = client.get( + sa_compute.service_endpoint + download_endpoint, query_string=payload + ) + assert response.status_code == 200, f"{response.data}" diff --git a/tests/test_initialize.py b/tests/test_initialize.py index 577975cc..939b07be 100644 --- a/tests/test_initialize.py +++ b/tests/test_initialize.py @@ -261,9 +261,8 @@ def test_initialize_compute_order_reused( publisher_wallet, consumer_wallet, True, - None, - free_c2d_env["consumerAddress"], - valid_until, + c2d_address=free_c2d_env["consumerAddress"], + valid_until=valid_until, timeout=60, c2d_environment=free_c2d_env["id"], )