diff --git a/src/components/Indexer/processor.ts b/src/components/Indexer/processor.ts index 455ecdaa9..e9c99b7e8 100644 --- a/src/components/Indexer/processor.ts +++ b/src/components/Indexer/processor.ts @@ -19,7 +19,6 @@ import ERC721Template from '@oceanprotocol/contracts/artifacts/contracts/templat import ERC20Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC20TemplateEnterprise.sol/ERC20TemplateEnterprise.json' assert { type: 'json' } import Dispenser from '@oceanprotocol/contracts/artifacts/contracts/pools/dispenser/Dispenser.sol/Dispenser.json' assert { type: 'json' } import FixedRateExchange from '@oceanprotocol/contracts/artifacts/contracts/pools/fixedRate/FixedRateExchange.sol/FixedRateExchange.json' assert { type: 'json' } -import AccessListContract from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessList.sol/AccessList.json' assert { type: 'json' } import { getDatabase } from '../../utils/database.js' import { PROTOCOL_COMMANDS, @@ -52,6 +51,7 @@ import { create256Hash } from '../../utils/crypt.js' import { URLUtils } from '../../utils/url.js' import { makeDid } from '../core/utils/validateDdoHandler.js' import { PolicyServer } from '../policyServer/index.js' +import { checkCredentialOnAccessList } from '../../utils/credentials.js' class BaseEventProcessor { protected networkId: number @@ -458,30 +458,17 @@ export class MetadataEventProcessor extends BaseEventProcessor { } if (authorizedPublishersList) { // check accessList - const chainsListed = Object.keys(authorizedPublishersList) - const chain = String(chainId) - // check the access lists for this chain - if (chainsListed.length > 0 && chainsListed.includes(chain)) { - let isAuthorized = false - for (const accessListAddress of authorizedPublishersList[chain]) { - const accessListContract = new ethers.Contract( - accessListAddress, - AccessListContract.abi, - signer - ) - // if has at least 1 token than is is authorized - const balance = await accessListContract.balanceOf(owner) - if (Number(balance) > 0) { - isAuthorized = true - break - } - } - if (!isAuthorized) { - INDEXER_LOGGER.error( - `DDO owner ${owner} is NOT part of the ${ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS_LIST.name} access group.` - ) - return - } + const isAuthorized = await checkCredentialOnAccessList( + authorizedPublishersList, + String(chainId), + owner, + signer + ) + if (!isAuthorized) { + INDEXER_LOGGER.error( + `DDO owner ${owner} is NOT part of the ${ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS_LIST.name} access group.` + ) + return } } diff --git a/src/components/Indexer/utils.ts b/src/components/Indexer/utils.ts index fb250cf9a..72211d6b2 100644 --- a/src/components/Indexer/utils.ts +++ b/src/components/Indexer/utils.ts @@ -307,7 +307,9 @@ export const processChunkLogs = async ( isAllowed = true // no rules for this specific chain, so ignore this } // move on to the next (do not process this event) - if (isAllowed === false) continue + if (isAllowed === false) { + continue + } } // end if (allowedValidatorsList) { } // end if if (checkMetadataValidated) { } diff --git a/src/components/core/handler/ddoHandler.ts b/src/components/core/handler/ddoHandler.ts index f787ec5d0..2992bc205 100644 --- a/src/components/core/handler/ddoHandler.ts +++ b/src/components/core/handler/ddoHandler.ts @@ -17,7 +17,6 @@ import { CORE_LOGGER } from '../../../utils/logging/common.js' import { Blockchain } from '../../../utils/blockchain.js' import { ethers, isAddress } from 'ethers' import ERC721Template from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC721Template.sol/ERC721Template.json' assert { type: 'json' } -import AccessListContract from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessList.sol/AccessList.json' assert { type: 'json' } // import lzma from 'lzma-native' import lzmajs from 'lzma-purejs-requirejs' import { @@ -44,6 +43,7 @@ import { wasNFTDeployedByOurFactory } from '../../Indexer/utils.js' import { deleteIndexedMetadataIfExists, validateDDOHash } from '../../../utils/asset.js' +import { checkCredentialOnAccessList } from '../../../utils/credentials.js' const MAX_NUM_PROVIDERS = 5 // after 60 seconds it returns whatever info we have available @@ -200,44 +200,25 @@ export class DecryptDdoHandler extends CommandHandler { } } - // access lit checks, needs blockchain connection + // access list checks, needs blockchain connection const { authorizedDecryptersList } = config - if (authorizedDecryptersList && Object.keys(authorizedDecryptersList).length > 0) { - // check accessList - const chainsListed = Object.keys(authorizedDecryptersList) - // check the access lists for this chain - if (chainsListed.length > 0 && chainsListed.includes(chainId)) { - let isAllowed = false - for (const accessListAddress of authorizedDecryptersList[chainId]) { - // instantiate contract and check balanceOf - const accessListContract = new ethers.Contract( - accessListAddress, - AccessListContract.abi, - blockchain.getSigner() - ) - - // check access list contract - const balance = await accessListContract.balanceOf( - await blockchain.getSigner().getAddress() - ) - if (Number(balance) > 0) { - isAllowed = true - break - } - } - if (!isAllowed) { - CORE_LOGGER.logMessage( - 'Decrypt DDO: Decrypter not authorized per access list', - true - ) - return { - stream: null, - status: { - httpStatus: 403, - error: 'Decrypt DDO: Decrypter not authorized per access list' - } - } + const isAllowed = await checkCredentialOnAccessList( + authorizedDecryptersList, + chainId, + decrypterAddress, + signer + ) + if (!isAllowed) { + CORE_LOGGER.logMessage( + 'Decrypt DDO: Decrypter not authorized per access list', + true + ) + return { + stream: null, + status: { + httpStatus: 403, + error: `Decrypt DDO: Decrypter ${decrypterAddress} not authorized per access list` } } } @@ -282,6 +263,7 @@ export class DecryptDdoHandler extends CommandHandler { try { encryptedDocument = ethers.getBytes(task.encryptedDocument) flags = Number(task.flags) + // eslint-disable-next-line prefer-destructuring documentHash = task.documentHash } catch (error) { CORE_LOGGER.logMessage(`Decrypt DDO: error ${error}`, true) diff --git a/src/test/integration/accessLists.test.ts b/src/test/integration/accessLists.test.ts new file mode 100644 index 000000000..3445f7bdb --- /dev/null +++ b/src/test/integration/accessLists.test.ts @@ -0,0 +1,201 @@ +import { + buildEnvOverrideConfig, + getMockSupportedNetworks, + OverrideEnvConfig, + setupEnvironment, + tearDownEnvironment, + TEST_ENV_CONFIG_FILE +} from '../utils/utils.js' +import { JsonRpcProvider, Signer } from 'ethers' +import { Blockchain } from '../../utils/blockchain.js' +import { RPCS, SupportedNetwork } from '../../@types/blockchain.js' +import { DEVELOPMENT_CHAIN_ID } from '../../utils/address.js' +import { ENVIRONMENT_VARIABLES } from '../../utils/constants.js' +import { deployAndGetAccessListConfig, EXISTING_ACCESSLISTS } from '../utils/contracts.js' +import { AccessListContract, OceanNodeConfig } from '../../@types/OceanNode.js' +import { homedir } from 'os' +import { getConfiguration } from '../../utils/config.js' +import { assert, expect } from 'chai' +import { findAccessListCredentials } from '../../utils/credentials.js' + +describe('Should deploy some accessLists before all other tests.', () => { + let config: OceanNodeConfig + let provider: JsonRpcProvider + const mockSupportedNetworks: RPCS = getMockSupportedNetworks() + + let previousConfiguration: OverrideEnvConfig[] + + let blockchain: Blockchain + // let contractAcessList: Contract + let owner: Signer + + let wallets: Signer[] = [] + + let allAccessListsDefinitions: AccessListContract[] = [] + + before(async () => { + provider = new JsonRpcProvider('http://127.0.0.1:8545') + config = await getConfiguration() // Force reload the configuration + + wallets = [ + (await provider.getSigner(0)) as Signer, + (await provider.getSigner(1)) as Signer, + (await provider.getSigner(2)) as Signer, + (await provider.getSigner(3)) as Signer + ] + + const rpcs: RPCS = config.supportedNetworks + const chain: SupportedNetwork = rpcs[String(DEVELOPMENT_CHAIN_ID)] + blockchain = new Blockchain( + chain.rpc, + chain.network, + chain.chainId, + chain.fallbackRPCs + ) + + owner = blockchain.getSigner() + + // ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS_LIST + const accessListPublishers = await deployAndGetAccessListConfig( + owner, + provider, + wallets + ) + EXISTING_ACCESSLISTS.set( + ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS_LIST.name, + accessListPublishers + ) + const accessListValidators = await deployAndGetAccessListConfig( + owner, + provider, + wallets + ) + // ENVIRONMENT_VARIABLES.ALLOWED_VALIDATORS_LIST + EXISTING_ACCESSLISTS.set( + ENVIRONMENT_VARIABLES.ALLOWED_VALIDATORS_LIST.name, + accessListValidators + ) + + const accessListDecrypters = await deployAndGetAccessListConfig( + owner, + provider, + wallets + ) + // ENVIRONMENT_VARIABLES.AUTHORIZED_DECRYPTERS_LIST + EXISTING_ACCESSLISTS.set( + ENVIRONMENT_VARIABLES.AUTHORIZED_DECRYPTERS_LIST.name, + accessListDecrypters + ) + + // put them all here + allAccessListsDefinitions = [ + accessListPublishers, + accessListValidators, + accessListDecrypters + ] + + // override and save configuration (always before calling getConfig()) + previousConfiguration = await setupEnvironment( + TEST_ENV_CONFIG_FILE, + buildEnvOverrideConfig( + [ + ENVIRONMENT_VARIABLES.RPCS, + ENVIRONMENT_VARIABLES.INDEXER_NETWORKS, + ENVIRONMENT_VARIABLES.PRIVATE_KEY, + ENVIRONMENT_VARIABLES.AUTHORIZED_DECRYPTERS, + ENVIRONMENT_VARIABLES.ALLOWED_ADMINS, + ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS, + ENVIRONMENT_VARIABLES.ADDRESS_FILE, + // ACCESS_LISTS + ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS_LIST, + ENVIRONMENT_VARIABLES.ALLOWED_VALIDATORS_LIST, + ENVIRONMENT_VARIABLES.AUTHORIZED_DECRYPTERS_LIST + ], + [ + JSON.stringify(mockSupportedNetworks), + JSON.stringify([8996]), + '0xc594c6e5def4bab63ac29eed19a134c130388f74f019bc74b8f4389df2837a58', + JSON.stringify(['0xe2DD09d719Da89e5a3D0F2549c7E24566e947260']), + JSON.stringify(['0xe2DD09d719Da89e5a3D0F2549c7E24566e947260']), + JSON.stringify([ + await owner.getAddress() // the node + ]), + `${homedir}/.ocean/ocean-contracts/artifacts/address.json`, + JSON.stringify( + EXISTING_ACCESSLISTS.get( + ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS_LIST.name + ) + ), + JSON.stringify( + EXISTING_ACCESSLISTS.get(ENVIRONMENT_VARIABLES.ALLOWED_VALIDATORS_LIST.name) + ), + JSON.stringify( + EXISTING_ACCESSLISTS.get( + ENVIRONMENT_VARIABLES.AUTHORIZED_DECRYPTERS_LIST.name + ) + ) + ] + ) + ) + + config = await getConfiguration() + }) + + it('should have some access lists', () => { + expect(EXISTING_ACCESSLISTS.size > 0, 'Should have at least 1 accessList') + }) + + it(`should have ${ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS_LIST.name} access lists`, () => { + assert( + config.authorizedPublishersList !== null, + `${ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS_LIST.name} accessList is not defined` + ) + console.log(config.authorizedPublishersList) + }) + + it(`should have ${ENVIRONMENT_VARIABLES.ALLOWED_VALIDATORS_LIST.name} access lists`, () => { + assert( + config.allowedValidatorsList !== null, + `${ENVIRONMENT_VARIABLES.ALLOWED_VALIDATORS_LIST.name} accessList is not defined` + ) + console.log(config.allowedValidatorsList) + }) + + it(`should have ${ENVIRONMENT_VARIABLES.AUTHORIZED_DECRYPTERS_LIST.name} access lists`, () => { + assert( + config.authorizedPublishersList !== null, + `${ENVIRONMENT_VARIABLES.AUTHORIZED_DECRYPTERS_LIST.name} accessList is not defined` + ) + console.log(config.authorizedPublishersList) + }) + + it('should check if wallets are on accessList (with Access)', async function () { + for (let z = 0; z < allAccessListsDefinitions.length; z++) { + const accessListAddress = allAccessListsDefinitions[z][DEVELOPMENT_CHAIN_ID][0] // we have only 1 accesslist per config + for (let i = 0; i < wallets.length; i++) { + const account = await wallets[i].getAddress() + expect( + (await findAccessListCredentials(owner, account, accessListAddress)) === true, + `Address ${account} has no balance on Access List ${accessListAddress}, so its not Authorized` + ) + } + } + }) + + it('should check that wallets are NOT on accessList (without Access)', async function () { + for (let z = 0; z < allAccessListsDefinitions.length; z++) { + const accessListAddress = allAccessListsDefinitions[z][DEVELOPMENT_CHAIN_ID][0] // we have only 1 accesslist per config + for (let i = wallets.length; i < 4; i++) { + const account = await (await provider.getSigner(i)).getAddress() + expect( + (await findAccessListCredentials(owner, account, accessListAddress)) === false, + `Address ${account} should not be part Access List ${accessListAddress}, therefore its not Authorized` + ) + } + } + }) + + after(async () => { + await tearDownEnvironment(previousConfiguration) + }) +}) diff --git a/src/test/integration/credentials.test.ts b/src/test/integration/credentials.test.ts index 372efe06a..3ebb272fd 100644 --- a/src/test/integration/credentials.test.ts +++ b/src/test/integration/credentials.test.ts @@ -27,7 +27,8 @@ import { ENVIRONMENT_VARIABLES, EVENTS, PROTOCOL_COMMANDS, - getConfiguration + getConfiguration, + printCurrentConfig } from '../../utils/index.js' import { DownloadHandler } from '../../components/core/handler/downloadHandler.js' import { GetDdoHandler } from '../../components/core/handler/ddoHandler.js' @@ -79,6 +80,9 @@ describe('Should run a complete node flow.', () => { let signer: Signer before(async () => { + provider = new JsonRpcProvider('http://127.0.0.1:8545') + publisherAccount = (await provider.getSigner(0)) as Signer + // override and save configuration (always before calling getConfig()) previousConfiguration = await setupEnvironment( TEST_ENV_CONFIG_FILE, @@ -89,6 +93,7 @@ describe('Should run a complete node flow.', () => { ENVIRONMENT_VARIABLES.PRIVATE_KEY, ENVIRONMENT_VARIABLES.AUTHORIZED_DECRYPTERS, ENVIRONMENT_VARIABLES.ALLOWED_ADMINS, + ENVIRONMENT_VARIABLES.AUTHORIZED_PUBLISHERS, ENVIRONMENT_VARIABLES.ADDRESS_FILE ], [ @@ -97,6 +102,9 @@ describe('Should run a complete node flow.', () => { '0xc594c6e5def4bab63ac29eed19a134c130388f74f019bc74b8f4389df2837a58', JSON.stringify(['0xe2DD09d719Da89e5a3D0F2549c7E24566e947260']), JSON.stringify(['0xe2DD09d719Da89e5a3D0F2549c7E24566e947260']), + JSON.stringify([ + await publisherAccount.getAddress() // signer 0 + ]), `${homedir}/.ocean/ocean-contracts/artifacts/address.json` ] ) @@ -117,9 +125,6 @@ describe('Should run a complete node flow.', () => { chain.fallbackRPCs ) - provider = new JsonRpcProvider('http://127.0.0.1:8545') - - publisherAccount = (await provider.getSigner(0)) as Signer consumerAccounts = [ (await provider.getSigner(1)) as Signer, (await provider.getSigner(2)) as Signer, @@ -344,6 +349,33 @@ describe('Should run a complete node flow.', () => { await doCheck() }) + it('should NOT allow to index the asset because address is not on AUTHORIZED_PUBLISHERS', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT * 2) + // this is not authorized + const nonAuthorizedAccount = (await provider.getSigner(4)) as Signer + const authorizedAccount = await publisherAccount.getAddress() + + printCurrentConfig() + expect( + config.authorizedPublishers.length === 1 && + config.authorizedPublishers[0] === authorizedAccount, + 'Unable to set AUTHORIZED_PUBLISHERS' + ) + + const publishedDataset = await publishAsset( + downloadAssetWithCredentials, + nonAuthorizedAccount + ) + + // will timeout + const { ddo, wasTimeout } = await waitToIndex( + publishedDataset?.ddo.id, + EVENTS.METADATA_CREATED, + DEFAULT_TEST_TIMEOUT + ) + assert(ddo === null && wasTimeout === true, 'DDO should NOT have been indexed') + }) + after(async () => { await tearDownEnvironment(previousConfiguration) oceanNode.getIndexer().stopAllThreads() diff --git a/src/test/integration/testUtils.ts b/src/test/integration/testUtils.ts index 16133389f..86cafc29b 100644 --- a/src/test/integration/testUtils.ts +++ b/src/test/integration/testUtils.ts @@ -7,7 +7,6 @@ import { DEFAULT_TEST_TIMEOUT } from '../utils/utils.js' import { getDatabase } from '../../utils/database.js' import { DDO } from '../../@types/DDO/DDO.js' import { sleep } from '../../utils/util.js' - // listen for indexer events export function addIndexerEventListener(eventName: string, ddoId: string, callback: any) { // add listener diff --git a/src/test/utils/contracts.ts b/src/test/utils/contracts.ts index 1426a8d80..06e1a14a8 100644 --- a/src/test/utils/contracts.ts +++ b/src/test/utils/contracts.ts @@ -1,5 +1,17 @@ -import { Contract, ethers, Signer } from 'ethers' +import { Contract, ethers, JsonRpcProvider, Signer } from 'ethers' +import { AccessListContract } from '../../@types' +import { + getOceanArtifactsAdressesByChainId, + DEVELOPMENT_CHAIN_ID, + getOceanArtifactsAdresses +} from '../../utils/address.js' +import AccessListFactory from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessListFactory.sol/AccessListFactory.json' assert { type: 'json' } +import AccessList from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessList.sol/AccessList.json' assert { type: 'json' } +export const EXISTING_ACCESSLISTS: Map = new Map< + string, + AccessListContract +>() /** * Returns a contract instance for the given address * @param {string} address - The address of the contract @@ -69,3 +81,49 @@ export async function deployAccessListContract( return null } } + +export async function deployAndGetAccessListConfig( + owner: Signer, + provider?: ethers.JsonRpcProvider, + wallets?: ethers.Signer[] +): Promise { + provider = provider || new JsonRpcProvider('http://127.0.0.1:8545') + let networkArtifacts = getOceanArtifactsAdressesByChainId(DEVELOPMENT_CHAIN_ID) + if (!networkArtifacts) { + networkArtifacts = getOceanArtifactsAdresses().development + } + + wallets = wallets || [ + (await provider.getSigner(0)) as Signer, + (await provider.getSigner(1)) as Signer, + (await provider.getSigner(2)) as Signer, + (await provider.getSigner(3)) as Signer + ] + const txAddress = await deployAccessListContract( + owner, // owner is first account + networkArtifacts.AccessListFactory, + AccessListFactory.abi, + 'AllowList', + 'ALLOW', + false, + await owner.getAddress(), + [ + await wallets[0].getAddress(), + await wallets[1].getAddress(), + await wallets[2].getAddress(), + await wallets[3].getAddress() + ], + ['https://oceanprotocol.com/nft/'] + ) + console.log('Successfully deployed AccessList at address: ', txAddress) + + const contractAcessList = getContract(txAddress, AccessList.abi, owner) + // console.log('contractAcessList:', contractAcessList) + if (contractAcessList) { + const result: AccessListContract = { + '8996': [txAddress] + } + return result + } + return null +} diff --git a/src/test/utils/hooks.ts b/src/test/utils/hooks.ts index 6b7e0e890..b2debece8 100644 --- a/src/test/utils/hooks.ts +++ b/src/test/utils/hooks.ts @@ -39,7 +39,7 @@ function getEnvOverrides(): OverrideEnvConfig[] { [ 'http://172.15.0.16:8080/', 'https://arweave.net/', - '{ "1": {"rpc": "https://rpc.eth.gateway.fm", "chainId": 1, "network": "mainet", "chunkSize": 100}, "137": {"rpc": "https://polygon.meowrpc.com", "chainId": 137, "network": "polygon", "chunkSize": 100 }}', + '{ "8996": {"rpc": "http://127.0.0.1:8545", "chainId": 8996, "network": "development", "chunkSize": 100}, "137": {"rpc": "https://polygon.meowrpc.com", "chainId": 137, "network": "polygon", "chunkSize": 100 }}', '0xc594c6e5def4bab63ac29eed19a134c130388f74f019bc74b8f4389df2837a58', SELECTED_RUN_DATABASE, SELECTED_RUN_DATABASE === DB_TYPES.ELASTIC_SEARCH @@ -53,6 +53,7 @@ export const mochaHooks = { beforeAll() { // get stuff we want to override envOverrides = getEnvOverrides() + // if it exists will use it, otherwise nothing happens // in any case it WILL NOT override the existing configuration // it returns the original object with the original value preserved to be restored later @@ -60,7 +61,9 @@ export const mochaHooks = { envOverrides = overrides }) initialSetupDone = true - CONFIG_LOGGER.debug(`(Hook) Initial test setup: ${JSON.stringify(envOverrides)} `) + CONFIG_LOGGER.debug( + `(Hook) Initial test setup: ${JSON.stringify(envOverrides, null, 4)} ` + ) // just in case the configuration value fails this.timeout(DEFAULT_TEST_TIMEOUT)