diff --git a/package-lock.json b/package-lock.json index fa784c81c..fda285d67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "@libp2p/websockets": "^8.1.1", "@multiformats/multiaddr": "^10.2.0", "@oceanprotocol/contracts": "^2.3.0", - "@oceanprotocol/ddo-js": "^0.0.6", + "@oceanprotocol/ddo-js": "^0.0.8", "@types/lodash.clonedeep": "^4.5.7", "axios": "^1.8.4", "base58-js": "^2.0.0", @@ -3562,26 +3562,17 @@ "license": "Apache-2.0" }, "node_modules/@oceanprotocol/ddo-js": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@oceanprotocol/ddo-js/-/ddo-js-0.0.6.tgz", - "integrity": "sha512-kmFmnm7Otn5jT9rPc0aIUV9wFs0Tqdx5bIZNtNNoNnhoBmTmV/zxdmJrZLylnqmywSHhjPGLyHZ2cmvwef2l4w==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@oceanprotocol/ddo-js/-/ddo-js-0.0.8.tgz", + "integrity": "sha512-Rv/vbsWjMEmwsLX57TYmJ0nuV3lOnJ4C6IKFvW14lfsN72e6i8kBshpFx7tYjpuVyW+WgyHvGOGjQmf0WBj0Mw==", "license": "Apache-2.0", "dependencies": { - "@rdfjs/dataset": "^2.0.2", "@rdfjs/formats-common": "^3.1.0", - "@rdfjs/types": "^1.1.2", - "@types/rdfjs__data-model": "^2.0.8", - "@types/rdfjs__dataset": "^2.0.7", "@types/rdfjs__formats-common": "^3.1.5", - "@types/rdfjs__parser-jsonld": "^2.1.7", - "@types/rdfjs__to-ntriples": "^3.0.0", "@zazuko/env-node": "^2.1.4", - "axios": "^1.7.9", "chai": "^5.1.2", - "crypto": "^1.0.1", "ethers": "^5.7.2", - "jose": "^5.9.6", - "lodash": "^4.17.21", + "rdf-literal": "^2.0.0", "rdf-validate-shacl": "^0.5.6" } }, @@ -4286,13 +4277,13 @@ "@ethersproject/strings": "^5.8.0" } }, - "node_modules/@oceanprotocol/ddo-js/node_modules/@types/rdfjs__to-ntriples": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/rdfjs__to-ntriples/-/rdfjs__to-ntriples-3.0.0.tgz", - "integrity": "sha512-3qZGpe2L3s2fAwmLvDDrcPCVDQmmEsg1KpwDd6bLPcCWQ7BISWHIQX/k/l1VU9EZB8uNoEAcmRmeVJY2jnu7wA==", + "node_modules/@oceanprotocol/ddo-js/node_modules/@rdfjs/types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rdfjs/types/-/types-2.0.1.tgz", + "integrity": "sha512-uyAzpugX7KekAXAHq26m3JlUIZJOC0uSBhpnefGV5i15bevDyyejoB7I+9MKeUrzXD8OOUI3+4FeV1wwQr5ihA==", "license": "MIT", "dependencies": { - "@rdfjs/types": ">=1.0.0" + "@types/node": "*" } }, "node_modules/@oceanprotocol/ddo-js/node_modules/aes-js": { @@ -4434,6 +4425,32 @@ "node": ">= 14.16" } }, + "node_modules/@oceanprotocol/ddo-js/node_modules/rdf-data-factory": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rdf-data-factory/-/rdf-data-factory-2.0.2.tgz", + "integrity": "sha512-WzPoYHwQYWvIP9k+7IBLY1b4nIDitzAK4mA37WumAF/Cjvu/KOtYJH9IPZnUTWNSd5K2+pq4vrcE9WZC4sRHhg==", + "license": "MIT", + "dependencies": { + "@rdfjs/types": "^2.0.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/rubensworks/" + } + }, + "node_modules/@oceanprotocol/ddo-js/node_modules/rdf-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rdf-literal/-/rdf-literal-2.0.0.tgz", + "integrity": "sha512-jlQ+h7EvnXmncmk8OzOYR8T3gNfd4g0LQXbflHkEkancic8dh0Tdt5RiRq8vUFndjIeNHt1RWeA5TAj6rgrtng==", + "license": "MIT", + "dependencies": { + "rdf-data-factory": "^2.0.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/rubensworks/" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -5448,6 +5465,7 @@ "resolved": "https://registry.npmjs.org/@types/rdfjs__data-model/-/rdfjs__data-model-2.0.9.tgz", "integrity": "sha512-rgQSlM9jr7XMZdC0xUIr0zsxf5FvdB4cxxzv+MlHm6uJGip5qi0q+BluNhakAzaM2I56nKLDqSE3I/XuOaHGnA==", "license": "MIT", + "peer": true, "dependencies": { "@rdfjs/types": "*" } @@ -5455,6 +5473,7 @@ "node_modules/@types/rdfjs__dataset": { "version": "2.0.7", "license": "MIT", + "peer": true, "dependencies": { "@rdfjs/types": "*" } @@ -7839,13 +7858,6 @@ "node": ">= 8" } }, - "node_modules/crypto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", - "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", - "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", - "license": "ISC" - }, "node_modules/crypto-random-string": { "version": "4.0.0", "dev": true, @@ -12162,15 +12174,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/js-sha3": { "version": "0.8.0", "license": "MIT" @@ -19066,4 +19069,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 67e7da726..0db7abe64 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,8 @@ "@libp2p/upnp-nat": "^1.2.1", "@libp2p/websockets": "^8.1.1", "@multiformats/multiaddr": "^10.2.0", + "@oceanprotocol/ddo-js": "^0.0.8", "@oceanprotocol/contracts": "^2.3.0", - "@oceanprotocol/ddo-js": "^0.0.6", "@types/lodash.clonedeep": "^4.5.7", "axios": "^1.8.4", "base58-js": "^2.0.0", diff --git a/src/@types/DDO/Credentials.ts b/src/@types/DDO/Credentials.ts deleted file mode 100644 index dc7c0dcc6..000000000 --- a/src/@types/DDO/Credentials.ts +++ /dev/null @@ -1 +0,0 @@ -export const KNOWN_CREDENTIALS_TYPES = ['address', 'accessList'] diff --git a/src/components/core/handler/downloadHandler.ts b/src/components/core/handler/downloadHandler.ts index 1ffec5438..f62fed198 100644 --- a/src/components/core/handler/downloadHandler.ts +++ b/src/components/core/handler/downloadHandler.ts @@ -291,7 +291,7 @@ export class DownloadHandler extends CommandHandler { ).success } else { accessGrantedDDOLevel = areKnownCredentialTypes(ddo.credentials) - ? checkCredentials(ddo.credentials, task.consumerAddress) + ? await checkCredentials(ddo.credentials, task.consumerAddress, ddo.chainId) : true } if (!accessGrantedDDOLevel) { @@ -396,7 +396,7 @@ export class DownloadHandler extends CommandHandler { ).success) } else { accessGrantedServiceLevel = areKnownCredentialTypes(service.credentials) - ? checkCredentials(service.credentials, task.consumerAddress) + ? await checkCredentials(service.credentials, task.consumerAddress, chainId) : true } diff --git a/src/test/data/assets.ts b/src/test/data/assets.ts index b59f69bf6..b458ccf31 100644 --- a/src/test/data/assets.ts +++ b/src/test/data/assets.ts @@ -1,4 +1,4 @@ -import { Credentials } from '@oceanprotocol/ddo-js' +import { Credentials, CREDENTIALS_TYPES } from '@oceanprotocol/ddo-js' export const downloadAsset = { '@context': ['https://w3id.org/did/v1'], @@ -62,29 +62,40 @@ export const downloadAsset = { const nftLevelCredentials: Credentials = { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0xBE5449a6A97aD46c8558A3356267Ee5D2731ab5e'] }, { - type: 'address', - values: ['0xA78deb2Fa79463945C247991075E2a0e98Ba7A09'] + type: CREDENTIALS_TYPES.ADDRESS, + values: [ + '0xA78deb2Fa79463945C247991075E2a0e98Ba7A09', + '0xe2DD09d719Da89e5a3D0F2549c7E24566e947260' + ] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x02354A1F160A3fd7ac8b02ee91F04104440B28E7'] } ] } const serviceLevelCredentials: Credentials = { - allow: [], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0xA78deb2Fa79463945C247991075E2a0e98Ba7A09'] } + ], + allow: [ + { + type: CREDENTIALS_TYPES.ADDRESS, + values: [ + '0xe2DD09d719Da89e5a3D0F2549c7E24566e947260', + '0xBE5449a6A97aD46c8558A3356267Ee5D2731ab5e' + ] + } ] } diff --git a/src/test/data/ddo.ts b/src/test/data/ddo.ts index 4044803b0..327a82dc6 100644 --- a/src/test/data/ddo.ts +++ b/src/test/data/ddo.ts @@ -1,3 +1,5 @@ +import { CREDENTIALS_TYPES } from '@oceanprotocol/ddo-js' + export const ddo = { hashType: 'sha256', '@context': ['https://w3id.org/did/v1'], @@ -71,13 +73,13 @@ export const genericAlgorithm = { credentials: { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0xBE5449a6A97aD46c8558A3356267Ee5D2731ab5e'] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x123'] } ] @@ -124,13 +126,13 @@ export const genericDDO = { credentials: { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0xBE5449a6A97aD46c8558A3356267Ee5D2731ab5e'] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x123'] } ] @@ -189,13 +191,13 @@ export const genericComputeDDO = { credentials: { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0xBE5449a6A97aD46c8558A3356267Ee5D2731ab5e'] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x123'] } ] @@ -348,13 +350,13 @@ export const ddov7 = { credentials: { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x1234'] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0xabcd'] } ] diff --git a/src/test/unit/credentials.test.ts b/src/test/unit/credentials.test.ts index be54a4ff3..5be299e36 100644 --- a/src/test/unit/credentials.test.ts +++ b/src/test/unit/credentials.test.ts @@ -12,7 +12,8 @@ import { } from '../utils/utils.js' import { ENVIRONMENT_VARIABLES } from '../../utils/constants.js' import { homedir } from 'os' -import { Credentials } from '@oceanprotocol/ddo-js' +import { DEVELOPMENT_CHAIN_ID } from '../../utils/address.js' +import { Credentials, CREDENTIALS_TYPES } from '@oceanprotocol/ddo-js' let envOverrides: OverrideEnvConfig[] @@ -28,133 +29,140 @@ describe('credentials', () => { envOverrides = await setupEnvironment(null, envOverrides) }) - it('should allow access with undefined or empty credentials', () => { + it('should deny access with undefined or empty credentials', async () => { const credentialsUndefined: Credentials = undefined const consumerAddress = '0x123' - const accessGranted1 = checkCredentials(credentialsUndefined, consumerAddress) - expect(accessGranted1).to.equal(true) + const accessGranted1 = await checkCredentials(credentialsUndefined, consumerAddress) + expect(accessGranted1).to.equal(false) const credentialsEmapty = {} as Credentials - const accessGranted2 = checkCredentials(credentialsEmapty, consumerAddress) - expect(accessGranted2).to.equal(true) + const accessGranted2 = await checkCredentials(credentialsEmapty, consumerAddress) + expect(accessGranted2).to.equal(false) }) - it('should allow access with empty allow and deny lists', () => { + + it('should deny access with empty allow and deny lists', async () => { + // if list does not exist or is empty access is denied const credentials: Credentials = { allow: [], deny: [] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) - expect(accessGranted).to.equal(true) + const accessGranted = await checkCredentials(credentials, consumerAddress) + expect(accessGranted).to.equal(false) }) - it('should allow access with empty values in deny lists', () => { + + it('should deny access with empty values in deny lists', async () => { const credentials: Credentials = { allow: [], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) - expect(accessGranted).to.equal(true) + const accessGranted = await checkCredentials(credentials, consumerAddress) + expect(accessGranted).to.equal(false) }) - it('should allow access with "accessList" credentials type', () => { + it('should deny access with "accessList" credentials (default behaviour if cannot check)', async () => { const consumerAddress = '0x123' const credentials: Credentials = { allow: [], deny: [ { - type: 'accessList', - values: [consumerAddress] + type: CREDENTIALS_TYPES.ACCESS_LIST, + values: [consumerAddress] // not a valid SC address anyway } ] } - const accessGranted = checkCredentials(credentials, consumerAddress) - expect(accessGranted).to.equal(true) + const accessGranted = await checkCredentials(credentials, consumerAddress) + expect(accessGranted).to.equal(false) }) - it('should deny access with empty values in allow lists', () => { + it('should deny access with empty values in allow lists', async () => { const credentials: Credentials = { deny: [], allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) + const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(false) }) - it('should allow access with address in allow list', () => { + + it('should allow access with address in allow list', async () => { const credentials: Credentials = { deny: [], allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x123'] } ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) + const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(true) }) - it('should allow access with address not in deny list', () => { + + it('should deny access with address not explicitly in deny list but also without any allow list', async () => { const credentials: Credentials = { allow: [], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x456'] } ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) - expect(accessGranted).to.equal(true) + const accessGranted = await checkCredentials(credentials, consumerAddress) + expect(accessGranted).to.equal(false) // its not denied explicitly but not allowed either }) - it('should deny access with address in deny list', () => { + + it('should deny access with address in deny list', async () => { const credentials: Credentials = { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x123'] } ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) + const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(false) }) - it('should deny access with address not in allow list', () => { + + it('should deny access with address not in allow list', async () => { const credentials: Credentials = { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x456'] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) + const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(false) }) @@ -163,6 +171,7 @@ describe('credentials', () => { allow: [], deny: [ { + // @ts-expect-error type: 'unknow_type', values: ['0x456'] } @@ -174,17 +183,17 @@ describe('credentials', () => { const credentialsOk: Credentials = { deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x456'] } ], allow: [ { - type: 'accessList', + type: CREDENTIALS_TYPES.ACCESS_LIST, values: ['0x456'] }, { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x678'] } ] @@ -195,12 +204,13 @@ describe('credentials', () => { const credentialsNOk: Credentials = { deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x456'] } ], allow: [ { + // @ts-expect-error type: 'not_valid_type', values: ['0x456'] } @@ -215,13 +225,13 @@ describe('credentials', () => { credentials: { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['*'] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x2222', '0x333'] } ] @@ -233,6 +243,79 @@ describe('credentials', () => { expect(hasAddressMatchAllRule(creds2.credentials.allow)).to.be.equal(false) }) + it('should deny access by default if no specific allow rule is a match', async () => { + const credentials: Credentials = { + allow: [ + { + type: CREDENTIALS_TYPES.ADDRESS, + values: [] + } + ], + deny: [ + { + type: CREDENTIALS_TYPES.ADDRESS, + values: [] + } + ] + } + const consumerAddress = '0x123' + const accessGranted = await checkCredentials( + credentials, + consumerAddress, + DEVELOPMENT_CHAIN_ID + ) + expect(accessGranted).to.equal(false) + }) + + it('should allow acess with match_allow: any', async () => { + const credentials: Credentials = { + allow: [ + { + type: CREDENTIALS_TYPES.ADDRESS, + values: ['0x123'] + }, + { + type: CREDENTIALS_TYPES.ACCESS_LIST, + values: ['0x456'] + } + ], + deny: [], + match_allow: 'any' + } + const consumerAddress = '0x123' + const accessGranted = await checkCredentials( + credentials, + consumerAddress, + DEVELOPMENT_CHAIN_ID + ) + + expect(accessGranted).to.equal(true) + }) + + it('should deny access with match_allow: all', async () => { + const credentials: Credentials = { + allow: [ + { + type: CREDENTIALS_TYPES.ADDRESS, + values: ['0x123'] + }, + { + type: CREDENTIALS_TYPES.ACCESS_LIST, + values: ['0x456'] + } + ], + deny: [], + match_allow: 'all' + } + const consumerAddress = '0x123' + const accessGranted = await checkCredentials( + credentials, + consumerAddress, + DEVELOPMENT_CHAIN_ID + ) + expect(accessGranted).to.equal(false) + }) + after(async () => { await tearDownEnvironment(envOverrides) }) diff --git a/src/utils/blockchain.ts b/src/utils/blockchain.ts index 3a175d04e..0a4f98f2e 100644 --- a/src/utils/blockchain.ts +++ b/src/utils/blockchain.ts @@ -14,7 +14,7 @@ import { import { getConfiguration } from './config.js' import { CORE_LOGGER } from './logging/common.js' import { sleep } from './util.js' -import { ConnectionStatus } from '../@types/blockchain.js' +import { ConnectionStatus, SupportedNetwork } from '../@types/blockchain.js' import { ValidateChainId } from '../@types/commands.js' import { KNOWN_CONFIDENTIAL_EVMS } from '../utils/address.js' @@ -294,3 +294,14 @@ export async function getJsonRpcProvider( } return new JsonRpcProvider(checkResult.networkRpc) } + +// useful for getting a Blockchain instance, as we repeat this piece of code often +export function getBlockchainHandler(network: SupportedNetwork): Blockchain { + const blockChain = new Blockchain( + network.rpc, + network.network, + network.chainId, + network.fallbackRPCs + ) + return blockChain +} diff --git a/src/utils/credentials.ts b/src/utils/credentials.ts index bc6c9306f..e3747d8b8 100644 --- a/src/utils/credentials.ts +++ b/src/utils/credentials.ts @@ -2,80 +2,164 @@ import { Contract, ethers, EventLog, Signer } from 'ethers' import AccessList from '@oceanprotocol/contracts/artifacts/contracts/accesslists/AccessList.sol/AccessList.json' assert { type: 'json' } import { AccessListContract } from '../@types/OceanNode.js' import { CORE_LOGGER } from './logging/common.js' - +import { + Credential, + Credentials, + CREDENTIALS_TYPES, + KNOWN_CREDENTIALS_TYPES +} from '@oceanprotocol/ddo-js' import { getNFTContract } from '../components/Indexer/utils.js' import { isDefined } from './util.js' +import { getConfiguration } from './config.js' +import { getBlockchainHandler } from './blockchain.js' + import { getOceanArtifactsAdressesByChainId } from './address.js' -import { Credential, Credentials } from '@oceanprotocol/ddo-js' -import { KNOWN_CREDENTIALS_TYPES } from '../@types/DDO/Credentials.js' -export function findCredential( - credentials: Credential[], +function isValidCredentialsList(credentials: Credential[]): boolean { + return Array.isArray(credentials) && credentials.length > 0 +} + +function isAddressCredentialMatch( + credential: Credential, consumerCredentials: Credential -) { - return credentials.find((credential) => { - if (Array.isArray(credential?.values)) { - if (credential.values.length > 0) { - const credentialType = String(credential?.type)?.toLowerCase() - const credentialValues = credential.values.map((v) => String(v)?.toLowerCase()) - return ( - credentialType === consumerCredentials.type && - credentialValues.includes(consumerCredentials.values[0]) - ) +): boolean { + if (credential?.type?.toLowerCase() !== CREDENTIALS_TYPES.ADDRESS) { + return false + } + + const credentialValues = credential.values.map((v) => String(v)?.toLowerCase()) + return credentialValues.includes(consumerCredentials.values[0]) +} + +function checkAddressCredential( + cred: Credential, + consumerCredentials: Credential, + credentials: Credentials +): boolean { + const accessAllow = isAddressCredentialMatch(cred, consumerCredentials) + if (accessAllow || isAddressMatchAll(cred)) { + return !credentials.match_allow || credentials.match_allow === 'any' + } + return false +} + +async function checkAccessListCredential( + cred: Credential, + consumerCredentials: Credential, + chainId?: number +): Promise { + if (cred.type !== CREDENTIALS_TYPES.ACCESS_LIST || !chainId) { + return false + } + + const config = await getConfiguration() + const supportedNetwork = config.supportedNetworks[String(chainId)] + if (!supportedNetwork) { + return false + } + + const blockChain = getBlockchainHandler(supportedNetwork) + for (const accessListAddress of cred.values) { + if ( + await findAccessListCredentials( + blockChain.getSigner(), + accessListAddress, + consumerCredentials.values[0] + ) + ) { + return true + } + } + return false +} + +function checkDenyList( + credentials: Credentials, + consumerCredentials: Credential +): boolean { + if (!isValidCredentialsList(credentials.deny)) { + return false + } + + let denyCount = 0 + for (const cred of credentials.deny) { + if (cred.type === CREDENTIALS_TYPES.ADDRESS) { + const accessDeny = isAddressCredentialMatch(cred, consumerCredentials) + if (accessDeny) { + if (!credentials.match_deny || credentials.match_deny === 'any') { + return true + } + denyCount++ } } + } + + return credentials.match_deny === 'all' && denyCount === credentials.deny.length +} + +async function checkAllowList( + credentials: Credentials, + consumerCredentials: Credential, + chainId?: number +): Promise { + if (!isValidCredentialsList(credentials.allow)) { return false - }) + } + + let matchCount = 0 + for (const cred of credentials.allow) { + if (cred.type === CREDENTIALS_TYPES.ADDRESS) { + if (checkAddressCredential(cred, consumerCredentials, credentials)) { + return true + } + matchCount++ + } else if (await checkAccessListCredential(cred, consumerCredentials, chainId)) { + return true + } + } + + return credentials.match_allow === 'all' && matchCount === credentials.allow.length +} + +function isAddressMatchAll(credential: Credential): boolean { + if (credential?.type?.toLowerCase() !== CREDENTIALS_TYPES.ADDRESS) { + return false + } + + return credential.values.some((value) => value?.toLowerCase() === '*') } export function hasAddressMatchAllRule(credentials: Credential[]): boolean { const creds = credentials.find((credential: Credential) => { if (Array.isArray(credential?.values)) { - if (credential.values.length > 0 && credential.type) { - const filteredValues: string[] = credential.values.filter((value: string) => { - return value?.toLowerCase() === '*' // address - }) - return ( - filteredValues.length > 0 && - credential.type.toLowerCase() === KNOWN_CREDENTIALS_TYPES[0] - ) - } + return isAddressMatchAll(credential) } return false }) return isDefined(creds) } -/** - * This method checks credentials - * @param credentials credentials - * @param consumerAddress consumer address - */ -export function checkCredentials(credentials: Credentials, consumerAddress: string) { +export async function checkCredentials( + credentials: Credentials, + consumerAddress: string, + chainId?: number +): Promise { + if (!credentials || (!credentials?.allow && !credentials?.deny)) { + return false + } + const consumerCredentials: Credential = { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [String(consumerAddress)?.toLowerCase()] } - const accessGranted = true - // check deny access - if (Array.isArray(credentials?.deny) && credentials.deny.length > 0) { - const accessDeny = findCredential(credentials.deny, consumerCredentials) - // credential is on deny list, so it should be blocked access - if (accessDeny) { - return false - } - // credential not found, so it really depends if we have a match - } - // check allow access - if (Array.isArray(credentials?.allow) && credentials.allow.length > 0) { - const accessAllow = findCredential(credentials.allow, consumerCredentials) - if (accessAllow || hasAddressMatchAllRule(credentials.allow)) { - return true - } + const isDenied = checkDenyList(credentials, consumerCredentials) + if (isDenied) { return false } - return accessGranted + + const isAllowed = await checkAllowList(credentials, consumerCredentials, chainId) + return isAllowed } export function areKnownCredentialTypes(credentials: Credentials): boolean { @@ -150,11 +234,18 @@ export async function findAccessListCredentials( contractAddress: string, address: string ): Promise { - const nftContract: ethers.Contract = getNFTContract(signer, contractAddress) - if (!nftContract) { + try { + const nftContract: ethers.Contract = getNFTContract(signer, contractAddress) + if (!nftContract) { + return false + } + return await findAccountFromAccessList(nftContract, address) + } catch (e) { + CORE_LOGGER.error( + `Unable to read accessList contract from address ${contractAddress}: ${e.message}` + ) return false } - return await findAccountFromAccessList(nftContract, address) } export async function findAccountFromAccessList(