From 969918f29e555eb703ca7180c22ccb158a213004 Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Wed, 26 Feb 2025 09:51:58 +0000 Subject: [PATCH 01/19] start refactoring creds behaviour for address --- src/utils/credentials.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/credentials.ts b/src/utils/credentials.ts index ec1174772..74d23c265 100644 --- a/src/utils/credentials.ts +++ b/src/utils/credentials.ts @@ -13,7 +13,7 @@ import { isDefined } from './util.js' export function findCredential( credentials: Credential[], consumerCredentials: Credential -) { +): Credential | undefined { return credentials.find((credential) => { if (Array.isArray(credential?.values)) { if (credential.values.length > 0) { From 2d638fd555d8a15e83ef5c560127466fb4416e8f Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Wed, 26 Feb 2025 12:20:08 +0000 Subject: [PATCH 02/19] more refactor --- src/@types/DDO/Credentials.ts | 5 ++ .../core/handler/downloadHandler.ts | 4 +- src/utils/credentials.ts | 80 ++++++++++++++----- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/@types/DDO/Credentials.ts b/src/@types/DDO/Credentials.ts index c8e3d6ea9..b29768dc7 100644 --- a/src/@types/DDO/Credentials.ts +++ b/src/@types/DDO/Credentials.ts @@ -1,5 +1,10 @@ // we will have more (user defined) export const KNOWN_CREDENTIALS_TYPES = ['address', 'accessList'] + +export const CREDENTIAL_TYPES = { + ADDRESS: KNOWN_CREDENTIALS_TYPES[0], + ACCESS_LIST: KNOWN_CREDENTIALS_TYPES[1] +} export interface Credential { type?: string values?: string[] diff --git a/src/components/core/handler/downloadHandler.ts b/src/components/core/handler/downloadHandler.ts index 1f06d6e65..3e0381b98 100644 --- a/src/components/core/handler/downloadHandler.ts +++ b/src/components/core/handler/downloadHandler.ts @@ -292,7 +292,7 @@ export class DownloadHandler extends Handler { ).success } else { accessGrantedDDOLevel = areKnownCredentialTypes(ddo.credentials) - ? checkCredentials(ddo.credentials, task.consumerAddress) + ? checkCredentials(ddo.credentials, task.consumerAddress, ddo.chainId) : true } if (!accessGrantedDDOLevel) { @@ -397,7 +397,7 @@ export class DownloadHandler extends Handler { ).success) } else { accessGrantedServiceLevel = areKnownCredentialTypes(service.credentials) - ? checkCredentials(service.credentials, task.consumerAddress) + ? checkCredentials(service.credentials, task.consumerAddress, chainId) : true } diff --git a/src/utils/credentials.ts b/src/utils/credentials.ts index 74d23c265..d5b85d99d 100644 --- a/src/utils/credentials.ts +++ b/src/utils/credentials.ts @@ -4,6 +4,7 @@ import { ethers, Signer } from 'ethers' import { CORE_LOGGER } from './logging/common.js' import { Credential, + CREDENTIAL_TYPES, Credentials, KNOWN_CREDENTIALS_TYPES } from '../@types/DDO/Credentials.js' @@ -29,18 +30,38 @@ export function findCredential( }) } +export function isAddressCredentialMatch( + credential: Credential, + consumerCredentials: Credential +): boolean { + if (credential?.type?.toLowerCase() !== CREDENTIAL_TYPES.ADDRESS) { + return false + } + if (credential.values.length > 0) { + const credentialValues = credential.values.map((v) => String(v)?.toLowerCase()) + return credentialValues.includes(consumerCredentials.values[0]) + } + + return false +} + +function isAddressMatchAll(credential: Credential): boolean { + if (credential?.type?.toLowerCase() !== CREDENTIAL_TYPES.ADDRESS) { + return false + } + if (credential.values.length > 0) { + const filteredValues: string[] = credential.values.filter((value: string) => { + return value?.toLowerCase() === '*' // address + }) + return filteredValues.length > 0 + } + return false +} + 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 }) @@ -52,27 +73,48 @@ export function hasAddressMatchAllRule(credentials: Credential[]): boolean { * @param credentials credentials * @param consumerAddress consumer address */ -export function checkCredentials(credentials: Credentials, consumerAddress: string) { +export function checkCredentials( + credentials: Credentials, + consumerAddress: string, + chainId?: number +) { const consumerCredentials: Credential = { - type: 'address', + type: CREDENTIAL_TYPES.ADDRESS, // '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 + for (const cred of credentials.deny) { + const { type } = cred + if (type === CREDENTIAL_TYPES.ADDRESS) { + const accessDeny = isAddressCredentialMatch(cred, 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 + } + // else TODO later + // support also for access list type here + // https://github.com/oceanprotocol/ocean-node/issues/840 + // else if (type === CREDENTIAL_TYPES.ACCESS_LIST && chainId) { + // } } - // 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 + for (const cred of credentials.allow) { + const { type } = cred + if (type === CREDENTIAL_TYPES.ADDRESS) { + const accessAllow = isAddressCredentialMatch(cred, consumerCredentials) + if (accessAllow || isAddressMatchAll(cred)) { + return true + } + } + // else if (type === CREDENTIAL_TYPES.ACCESS_LIST && chainId) { + // } } return false } From 2c23a9b6a8d8e6f62734e22c1866cfae7756db7b Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Thu, 27 Feb 2025 14:57:45 +0000 Subject: [PATCH 03/19] change default behaviour --- src/utils/credentials.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/credentials.ts b/src/utils/credentials.ts index d5b85d99d..53691839c 100644 --- a/src/utils/credentials.ts +++ b/src/utils/credentials.ts @@ -83,8 +83,10 @@ export function checkCredentials( values: [String(consumerAddress)?.toLowerCase()] } - const accessGranted = true + const accessGranted = false // check deny access + // https://github.com/oceanprotocol/ocean-node/issues/810 + // for deny rules: if value does not exist or it's empty -> there is no deny list. if value list has at least one element, check it if (Array.isArray(credentials?.deny) && credentials.deny.length > 0) { for (const cred of credentials.deny) { const { type } = cred @@ -94,7 +96,7 @@ export function checkCredentials( if (accessDeny) { return false } - // credential not found, so it really depends if we have a match + // credential not found, so it really depends if we have a match on the allow list instead } // else TODO later // support also for access list type here @@ -104,6 +106,7 @@ export function checkCredentials( } } // check allow access + // for allow rules: if value does not exist or it's empty -> no one has access. if value list has at least one element, check it if (Array.isArray(credentials?.allow) && credentials.allow.length > 0) { for (const cred of credentials.allow) { const { type } = cred @@ -116,7 +119,6 @@ export function checkCredentials( // else if (type === CREDENTIAL_TYPES.ACCESS_LIST && chainId) { // } } - return false } return accessGranted } From e9f98d54e84097f3912dada64b6d462bb6fd5cdb Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Thu, 27 Feb 2025 15:03:01 +0000 Subject: [PATCH 04/19] change default behaviour, add unti test --- src/test/unit/credentials.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/test/unit/credentials.test.ts b/src/test/unit/credentials.test.ts index 7b2d1534f..bfdcbd12f 100644 --- a/src/test/unit/credentials.test.ts +++ b/src/test/unit/credentials.test.ts @@ -13,6 +13,7 @@ import { } from '../utils/utils.js' import { ENVIRONMENT_VARIABLES } from '../../utils/constants.js' import { homedir } from 'os' +import { DEVELOPMENT_CHAIN_ID } from '../../utils/address.js' let envOverrides: OverrideEnvConfig[] @@ -227,6 +228,30 @@ 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', () => { + const credentials: Credentials = { + allow: [ + { + type: 'address', + values: [] + } + ], + deny: [ + { + type: 'address', + values: [] + } + ] + } + const consumerAddress = '0x123' + const accessGranted = checkCredentials( + credentials, + consumerAddress, + DEVELOPMENT_CHAIN_ID + ) + expect(accessGranted).to.equal(false) + }) + after(async () => { await tearDownEnvironment(envOverrides) }) From 97f56a036499f457e583953404c170dc698442e8 Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Thu, 27 Feb 2025 15:13:24 +0000 Subject: [PATCH 05/19] fix unit test --- src/test/unit/credentials.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/unit/credentials.test.ts b/src/test/unit/credentials.test.ts index bfdcbd12f..d0f3a090c 100644 --- a/src/test/unit/credentials.test.ts +++ b/src/test/unit/credentials.test.ts @@ -29,14 +29,14 @@ 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', () => { const credentialsUndefined: Credentials = undefined const consumerAddress = '0x123' const accessGranted1 = checkCredentials(credentialsUndefined, consumerAddress) - expect(accessGranted1).to.equal(true) + expect(accessGranted1).to.equal(false) const credentialsEmapty = {} as Credentials const accessGranted2 = checkCredentials(credentialsEmapty, consumerAddress) - expect(accessGranted2).to.equal(true) + expect(accessGranted2).to.equal(false) }) it('should allow access with empty allow and deny lists', () => { const credentials: Credentials = { @@ -47,7 +47,7 @@ describe('credentials', () => { const accessGranted = checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(true) }) - it('should allow access with empty values in deny lists', () => { + it('should deny access with empty values in deny lists', () => { const credentials: Credentials = { deny: [ { @@ -58,10 +58,10 @@ describe('credentials', () => { } const consumerAddress = '0x123' const accessGranted = checkCredentials(credentials, consumerAddress) - expect(accessGranted).to.equal(true) + 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)', () => { const consumerAddress = '0x123' const credentials: Credentials = { deny: [ From 86318832a47871bfa8ca21ff725ac021dbc00a6d Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Thu, 27 Feb 2025 15:25:44 +0000 Subject: [PATCH 06/19] fix unit test --- src/test/unit/credentials.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/unit/credentials.test.ts b/src/test/unit/credentials.test.ts index d0f3a090c..e21a2e77c 100644 --- a/src/test/unit/credentials.test.ts +++ b/src/test/unit/credentials.test.ts @@ -38,14 +38,15 @@ describe('credentials', () => { const accessGranted2 = 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', () => { + // 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) + expect(accessGranted).to.equal(false) }) it('should deny access with empty values in deny lists', () => { const credentials: Credentials = { From 3836ce3d837573e652de20358298918e132e59a2 Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Thu, 27 Feb 2025 15:35:18 +0000 Subject: [PATCH 07/19] fix unit test --- src/test/unit/credentials.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/unit/credentials.test.ts b/src/test/unit/credentials.test.ts index e21a2e77c..3b69e5d57 100644 --- a/src/test/unit/credentials.test.ts +++ b/src/test/unit/credentials.test.ts @@ -68,13 +68,13 @@ describe('credentials', () => { deny: [ { type: 'accessList', - values: [consumerAddress] + values: [consumerAddress] // not a valid SC address anyway } ] } const accessGranted = checkCredentials(credentials, consumerAddress) - expect(accessGranted).to.equal(true) + expect(accessGranted).to.equal(false) }) it('should deny access with empty values in allow lists', () => { From e9db2c46df033ed05922ca8ff9ec7c71797c896e Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Thu, 27 Feb 2025 15:40:13 +0000 Subject: [PATCH 08/19] fix unit test --- src/test/unit/credentials.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/unit/credentials.test.ts b/src/test/unit/credentials.test.ts index 3b69e5d57..a91896380 100644 --- a/src/test/unit/credentials.test.ts +++ b/src/test/unit/credentials.test.ts @@ -103,7 +103,7 @@ describe('credentials', () => { const accessGranted = 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', () => { const credentials: Credentials = { deny: [ { @@ -114,7 +114,7 @@ describe('credentials', () => { } const consumerAddress = '0x123' const accessGranted = checkCredentials(credentials, consumerAddress) - expect(accessGranted).to.equal(true) + expect(accessGranted).to.equal(false) // its not denied explicitly but not allowed either }) it('should deny access with address in deny list', () => { const credentials: Credentials = { From 56e795389eadb61eb063c572f82dd962d9cc4865 Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Thu, 27 Feb 2025 15:56:54 +0000 Subject: [PATCH 09/19] update asset credentials --- src/test/data/assets.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/test/data/assets.ts b/src/test/data/assets.ts index 476b78850..92b9cd48e 100644 --- a/src/test/data/assets.ts +++ b/src/test/data/assets.ts @@ -68,7 +68,10 @@ const nftLevelCredentials: Credentials = { }, { type: 'address', - values: ['0xA78deb2Fa79463945C247991075E2a0e98Ba7A09'] + values: [ + '0xA78deb2Fa79463945C247991075E2a0e98Ba7A09', + '0xe2DD09d719Da89e5a3D0F2549c7E24566e947260' + ] } ], deny: [ @@ -85,6 +88,12 @@ const serviceLevelCredentials: Credentials = { type: 'address', values: ['0xA78deb2Fa79463945C247991075E2a0e98Ba7A09'] } + ], + allow: [ + { + type: 'address', + values: ['0xe2DD09d719Da89e5a3D0F2549c7E24566e947260'] + } ] } From ae79d688fed8efdfca630c834cc6aa6d7653785c Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Fri, 28 Feb 2025 08:41:03 +0000 Subject: [PATCH 10/19] try debug it --- src/components/core/handler/downloadHandler.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/core/handler/downloadHandler.ts b/src/components/core/handler/downloadHandler.ts index 3e0381b98..e8e2da262 100644 --- a/src/components/core/handler/downloadHandler.ts +++ b/src/components/core/handler/downloadHandler.ts @@ -276,6 +276,7 @@ export class DownloadHandler extends Handler { // check credentials (DDO level) let accessGrantedDDOLevel: boolean if (ddo.credentials) { + console.log('Checking ddo level credentials:', ddo.credentials) // if POLICY_SERVER_URL exists, then ocean-node will NOT perform any checks. // It will just use the existing code and let PolicyServer decide. if (isPolicyServerConfigured()) { @@ -294,6 +295,11 @@ export class DownloadHandler extends Handler { accessGrantedDDOLevel = areKnownCredentialTypes(ddo.credentials) ? checkCredentials(ddo.credentials, task.consumerAddress, ddo.chainId) : true + console.log( + 'accessGrantedDDOLevel, task.consumerAddress', + accessGrantedDDOLevel, + task.consumerAddress + ) } if (!accessGrantedDDOLevel) { CORE_LOGGER.logMessage(`Error: Access to asset ${ddo.id} was denied`, true) @@ -379,6 +385,7 @@ export class DownloadHandler extends Handler { // if using a policy server and we are here it means that access was granted (they are merged/assessed together) if (service.credentials) { let accessGrantedServiceLevel: boolean + console.log('service level credentials: ', service.credentials) if (isPolicyServerConfigured()) { // we use the previous check or we do it again // (in case there is no DDO level credentials and we only have Service level ones) @@ -399,6 +406,11 @@ export class DownloadHandler extends Handler { accessGrantedServiceLevel = areKnownCredentialTypes(service.credentials) ? checkCredentials(service.credentials, task.consumerAddress, chainId) : true + console.log( + 'accessGrantedServiceLevel, task.consumerAddress: ', + accessGrantedServiceLevel, + task.consumerAddress + ) } if (!accessGrantedServiceLevel) { From edbdd127012f6dc6b744c3c15f5ac305d754ec34 Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Fri, 28 Feb 2025 09:15:50 +0000 Subject: [PATCH 11/19] fix service levels credentials on asset --- src/test/data/assets.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/data/assets.ts b/src/test/data/assets.ts index 92b9cd48e..728940b7d 100644 --- a/src/test/data/assets.ts +++ b/src/test/data/assets.ts @@ -92,7 +92,10 @@ const serviceLevelCredentials: Credentials = { allow: [ { type: 'address', - values: ['0xe2DD09d719Da89e5a3D0F2549c7E24566e947260'] + values: [ + '0xe2DD09d719Da89e5a3D0F2549c7E24566e947260', + '0xBE5449a6A97aD46c8558A3356267Ee5D2731ab5e' + ] } ] } From af5b509551fee88a188c9734ee7eb7faefdb78c7 Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Fri, 28 Feb 2025 09:30:34 +0000 Subject: [PATCH 12/19] clean debug console.log --- src/components/core/handler/downloadHandler.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/components/core/handler/downloadHandler.ts b/src/components/core/handler/downloadHandler.ts index e8e2da262..3e0381b98 100644 --- a/src/components/core/handler/downloadHandler.ts +++ b/src/components/core/handler/downloadHandler.ts @@ -276,7 +276,6 @@ export class DownloadHandler extends Handler { // check credentials (DDO level) let accessGrantedDDOLevel: boolean if (ddo.credentials) { - console.log('Checking ddo level credentials:', ddo.credentials) // if POLICY_SERVER_URL exists, then ocean-node will NOT perform any checks. // It will just use the existing code and let PolicyServer decide. if (isPolicyServerConfigured()) { @@ -295,11 +294,6 @@ export class DownloadHandler extends Handler { accessGrantedDDOLevel = areKnownCredentialTypes(ddo.credentials) ? checkCredentials(ddo.credentials, task.consumerAddress, ddo.chainId) : true - console.log( - 'accessGrantedDDOLevel, task.consumerAddress', - accessGrantedDDOLevel, - task.consumerAddress - ) } if (!accessGrantedDDOLevel) { CORE_LOGGER.logMessage(`Error: Access to asset ${ddo.id} was denied`, true) @@ -385,7 +379,6 @@ export class DownloadHandler extends Handler { // if using a policy server and we are here it means that access was granted (they are merged/assessed together) if (service.credentials) { let accessGrantedServiceLevel: boolean - console.log('service level credentials: ', service.credentials) if (isPolicyServerConfigured()) { // we use the previous check or we do it again // (in case there is no DDO level credentials and we only have Service level ones) @@ -406,11 +399,6 @@ export class DownloadHandler extends Handler { accessGrantedServiceLevel = areKnownCredentialTypes(service.credentials) ? checkCredentials(service.credentials, task.consumerAddress, chainId) : true - console.log( - 'accessGrantedServiceLevel, task.consumerAddress: ', - accessGrantedServiceLevel, - task.consumerAddress - ) } if (!accessGrantedServiceLevel) { From a4498fce88643454bd5e92675fe5ebf5f2af4a13 Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Fri, 7 Mar 2025 09:36:43 +0000 Subject: [PATCH 13/19] add new types and match all/any rules --- src/@types/DDO/Credentials.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/@types/DDO/Credentials.ts b/src/@types/DDO/Credentials.ts index b29768dc7..f8828291a 100644 --- a/src/@types/DDO/Credentials.ts +++ b/src/@types/DDO/Credentials.ts @@ -1,16 +1,21 @@ // we will have more (user defined) -export const KNOWN_CREDENTIALS_TYPES = ['address', 'accessList'] +export const KNOWN_CREDENTIALS_TYPES = ['address', 'accessList'] // the ones we handle export const CREDENTIAL_TYPES = { ADDRESS: KNOWN_CREDENTIALS_TYPES[0], - ACCESS_LIST: KNOWN_CREDENTIALS_TYPES[1] + ACCESS_LIST: KNOWN_CREDENTIALS_TYPES[1], + POLICY_SERVER_SPECIFIC: 'PS-specific Type' // externally handled by Policy Server } export interface Credential { type?: string values?: string[] } +export type MATCH_RULES = 'any' | 'all' + export interface Credentials { + match_allow?: MATCH_RULES // any => it's enough to have one rule matched, all => all allow rules should match, default: 'all' + match_deny: MATCH_RULES // same pattern as above, default is 'any' allow?: Credential[] deny?: Credential[] } From 01e249aa34675882baad390036f561bf73828e39 Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Fri, 7 Mar 2025 09:47:03 +0000 Subject: [PATCH 14/19] add new types and match all/any rules --- src/@types/DDO/Credentials.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/@types/DDO/Credentials.ts b/src/@types/DDO/Credentials.ts index f8828291a..feb8b1425 100644 --- a/src/@types/DDO/Credentials.ts +++ b/src/@types/DDO/Credentials.ts @@ -15,7 +15,7 @@ export type MATCH_RULES = 'any' | 'all' export interface Credentials { match_allow?: MATCH_RULES // any => it's enough to have one rule matched, all => all allow rules should match, default: 'all' - match_deny: MATCH_RULES // same pattern as above, default is 'any' + match_deny?: MATCH_RULES // same pattern as above, default is 'any' allow?: Credential[] deny?: Credential[] } From ed6263791267d135a141b9b8488f245165e10f21 Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Mon, 10 Mar 2025 11:20:52 +0000 Subject: [PATCH 15/19] extend check to match rules any or all --- src/utils/credentials.ts | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/utils/credentials.ts b/src/utils/credentials.ts index 53691839c..98a24caa7 100644 --- a/src/utils/credentials.ts +++ b/src/utils/credentials.ts @@ -83,42 +83,59 @@ export function checkCredentials( values: [String(consumerAddress)?.toLowerCase()] } + // if no address-based credentials are defined (both allow and deny lists are empty), access to the asset is restricted to everybody; + // to allow access to everybody, the symbol * will be used in the allow list; + // if a web3 address is present on both deny and allow lists, the deny list takes precedence + // and access to the asset is denied for the respective address. const accessGranted = false // check deny access // https://github.com/oceanprotocol/ocean-node/issues/810 // for deny rules: if value does not exist or it's empty -> there is no deny list. if value list has at least one element, check it + if (Array.isArray(credentials?.deny) && credentials.deny.length > 0) { + let denyCount = 0 for (const cred of credentials.deny) { const { type } = cred if (type === CREDENTIAL_TYPES.ADDRESS) { const accessDeny = isAddressCredentialMatch(cred, consumerCredentials) // credential is on deny list, so it should be blocked access if (accessDeny) { - return false + if (!isDefined(credentials.match_deny) || credentials.match_deny === 'any') { + return false + } } + denyCount++ // credential not found, so it really depends if we have a match on the allow list instead } - // else TODO later - // support also for access list type here - // https://github.com/oceanprotocol/ocean-node/issues/840 - // else if (type === CREDENTIAL_TYPES.ACCESS_LIST && chainId) { - // } + } + if (credentials.match_deny === 'all' && denyCount === credentials.deny.length) { + return false } } // check allow access // for allow rules: if value does not exist or it's empty -> no one has access. if value list has at least one element, check it if (Array.isArray(credentials?.allow) && credentials.allow.length > 0) { + let matchCount = 0 for (const cred of credentials.allow) { const { type } = cred if (type === CREDENTIAL_TYPES.ADDRESS) { const accessAllow = isAddressCredentialMatch(cred, consumerCredentials) if (accessAllow || isAddressMatchAll(cred)) { - return true + // if no match_allow or 'any', its fine + if (!isDefined(credentials.match_allow) || credentials.match_allow === 'any') { + return true + } + // otherwise, match 'all', in this case the amount of matches should be the same of the amount of rules + matchCount++ } } + // extend function to ACCESS_LIST (https://github.com/oceanprotocol/ocean-node/issues/804) // else if (type === CREDENTIAL_TYPES.ACCESS_LIST && chainId) { // } } + if (credentials.match_allow === 'all' && matchCount === credentials.allow.length) { + return true + } } return accessGranted } From f1b87b6a8287fbf1185f0d04421e1d1b12e9005c Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Tue, 11 Mar 2025 09:41:33 +0000 Subject: [PATCH 16/19] add support for accessList on download checkCredentials --- .../core/handler/downloadHandler.ts | 4 +-- src/utils/blockchain.ts | 13 ++++++- src/utils/credentials.ts | 35 +++++++++++++++---- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/components/core/handler/downloadHandler.ts b/src/components/core/handler/downloadHandler.ts index 084276dfa..ecf7aebee 100644 --- a/src/components/core/handler/downloadHandler.ts +++ b/src/components/core/handler/downloadHandler.ts @@ -292,7 +292,7 @@ export class DownloadHandler extends Handler { ).success } else { accessGrantedDDOLevel = areKnownCredentialTypes(ddo.credentials) - ? checkCredentials(ddo.credentials, task.consumerAddress, ddo.chainId) + ? await checkCredentials(ddo.credentials, task.consumerAddress, ddo.chainId) : true } if (!accessGrantedDDOLevel) { @@ -397,7 +397,7 @@ export class DownloadHandler extends Handler { ).success) } else { accessGrantedServiceLevel = areKnownCredentialTypes(service.credentials) - ? checkCredentials(service.credentials, task.consumerAddress, chainId) + ? await checkCredentials(service.credentials, task.consumerAddress, chainId) : true } diff --git a/src/utils/blockchain.ts b/src/utils/blockchain.ts index bfafb5665..e0b9f76d6 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' export class Blockchain { @@ -218,3 +218,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 98a24caa7..6804699f6 100644 --- a/src/utils/credentials.ts +++ b/src/utils/credentials.ts @@ -10,6 +10,8 @@ import { } from '../@types/DDO/Credentials.js' import { getNFTContract } from '../components/Indexer/utils.js' import { isDefined } from './util.js' +import { getConfiguration } from './config.js' +import { getBlockchainHandler } from './blockchain.js' export function findCredential( credentials: Credential[], @@ -73,11 +75,11 @@ export function hasAddressMatchAllRule(credentials: Credential[]): boolean { * @param credentials credentials * @param consumerAddress consumer address */ -export function checkCredentials( +export async function checkCredentials( credentials: Credentials, consumerAddress: string, chainId?: number -) { +): Promise { const consumerCredentials: Credential = { type: CREDENTIAL_TYPES.ADDRESS, // 'address', values: [String(consumerAddress)?.toLowerCase()] @@ -128,10 +130,22 @@ export function checkCredentials( // otherwise, match 'all', in this case the amount of matches should be the same of the amount of rules matchCount++ } + } else if (type === CREDENTIAL_TYPES.ACCESS_LIST && chainId) { + const config = await getConfiguration() + const supportedNetwork = config.supportedNetworks[String(chainId)] + if (supportedNetwork) { + const blockChain = getBlockchainHandler(supportedNetwork) + for (const accessListContractAddress of cred.values) { + const balanceOk = await findAccessListCredentials( + blockChain.getSigner(), + accessListContractAddress, + consumerAddress + ) + if (balanceOk) return true + } + } } // extend function to ACCESS_LIST (https://github.com/oceanprotocol/ocean-node/issues/804) - // else if (type === CREDENTIAL_TYPES.ACCESS_LIST && chainId) { - // } } if (credentials.match_allow === 'all' && matchCount === credentials.allow.length) { return true @@ -212,11 +226,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( From 329fb953da45beaa75654a4d092617d6fe5ccebe Mon Sep 17 00:00:00 2001 From: paulo-ocean Date: Tue, 11 Mar 2025 10:34:33 +0000 Subject: [PATCH 17/19] fix tests, async call --- src/test/unit/credentials.test.ts | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/test/unit/credentials.test.ts b/src/test/unit/credentials.test.ts index a91896380..fd5d4691e 100644 --- a/src/test/unit/credentials.test.ts +++ b/src/test/unit/credentials.test.ts @@ -29,26 +29,26 @@ describe('credentials', () => { envOverrides = await setupEnvironment(null, envOverrides) }) - it('should deny 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) + const accessGranted1 = await checkCredentials(credentialsUndefined, consumerAddress) expect(accessGranted1).to.equal(false) const credentialsEmapty = {} as Credentials - const accessGranted2 = checkCredentials(credentialsEmapty, consumerAddress) + const accessGranted2 = await checkCredentials(credentialsEmapty, consumerAddress) expect(accessGranted2).to.equal(false) }) - it('should deny 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) + const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(false) }) - it('should deny access with empty values in deny lists', () => { + it('should deny access with empty values in deny lists', async () => { const credentials: Credentials = { deny: [ { @@ -58,11 +58,11 @@ describe('credentials', () => { ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) + const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(false) }) - it('should deny access with "accessList" credentials (default behaviour if cannot check)', () => { + it('should deny access with "accessList" credentials (default behaviour if cannot check)', async () => { const consumerAddress = '0x123' const credentials: Credentials = { deny: [ @@ -73,11 +73,11 @@ describe('credentials', () => { ] } - const accessGranted = checkCredentials(credentials, consumerAddress) + 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 = { allow: [ { @@ -87,10 +87,10 @@ describe('credentials', () => { ] } 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 = { allow: [ { @@ -100,10 +100,10 @@ describe('credentials', () => { ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) + const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(true) }) - it('should deny access with address not explicitly in deny list but also without any allow list', () => { + it('should deny access with address not explicitly in deny list but also without any allow list', async () => { const credentials: Credentials = { deny: [ { @@ -113,10 +113,10 @@ describe('credentials', () => { ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) + 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: [ { @@ -132,10 +132,10 @@ describe('credentials', () => { ] } 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: [ { @@ -151,7 +151,7 @@ describe('credentials', () => { ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials(credentials, consumerAddress) + const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(false) }) @@ -229,7 +229,7 @@ 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', () => { + it('should deny access by default if no specific allow rule is a match', async () => { const credentials: Credentials = { allow: [ { @@ -245,7 +245,7 @@ describe('credentials', () => { ] } const consumerAddress = '0x123' - const accessGranted = checkCredentials( + const accessGranted = await checkCredentials( credentials, consumerAddress, DEVELOPMENT_CHAIN_ID From b9b5d7740e46da6776e04491786241b9cdf11bb4 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 23 Apr 2025 09:42:48 +0300 Subject: [PATCH 18/19] use latest ddo-js --- package-lock.json | 8 +++---- package.json | 2 +- src/@types/DDO/Credentials.ts | 21 ------------------ src/test/data/assets.ts | 12 +++++----- src/test/data/ddo.ts | 18 ++++++++------- src/test/unit/credentials.test.ts | 37 +++++++++++++++++-------------- src/utils/credentials.ts | 16 ++++++------- 7 files changed, 49 insertions(+), 65 deletions(-) delete mode 100644 src/@types/DDO/Credentials.ts diff --git a/package-lock.json b/package-lock.json index 4edd61a48..2ee64b81d 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-next.3", - "@oceanprotocol/ddo-js": "^0.0.6", + "@oceanprotocol/ddo-js": "^0.0.7", "@types/lodash.clonedeep": "^4.5.7", "axios": "^1.8.4", "base58-js": "^2.0.0", @@ -3562,9 +3562,9 @@ "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.7", + "resolved": "https://registry.npmjs.org/@oceanprotocol/ddo-js/-/ddo-js-0.0.7.tgz", + "integrity": "sha512-Az+8aFPoTOdhYYNEdVgPxhUc29pwkznRIM5hv/9HkPUd7HTBR5e/Dps/hf6C6KMDF5FCUyvtb3QMOIFPEdHMSg==", "license": "Apache-2.0", "dependencies": { "@rdfjs/dataset": "^2.0.2", diff --git a/package.json b/package.json index 626efde2b..19eb47bc0 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.6", "@oceanprotocol/contracts": "^2.3.0-next.3", + "@oceanprotocol/ddo-js": "^0.0.7", "@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 feb8b1425..000000000 --- a/src/@types/DDO/Credentials.ts +++ /dev/null @@ -1,21 +0,0 @@ -// we will have more (user defined) -export const KNOWN_CREDENTIALS_TYPES = ['address', 'accessList'] // the ones we handle - -export const CREDENTIAL_TYPES = { - ADDRESS: KNOWN_CREDENTIALS_TYPES[0], - ACCESS_LIST: KNOWN_CREDENTIALS_TYPES[1], - POLICY_SERVER_SPECIFIC: 'PS-specific Type' // externally handled by Policy Server -} -export interface Credential { - type?: string - values?: string[] -} - -export type MATCH_RULES = 'any' | 'all' - -export interface Credentials { - match_allow?: MATCH_RULES // any => it's enough to have one rule matched, all => all allow rules should match, default: 'all' - match_deny?: MATCH_RULES // same pattern as above, default is 'any' - allow?: Credential[] - deny?: Credential[] -} diff --git a/src/test/data/assets.ts b/src/test/data/assets.ts index 52df4fd2e..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,11 +62,11 @@ export const downloadAsset = { const nftLevelCredentials: Credentials = { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0xBE5449a6A97aD46c8558A3356267Ee5D2731ab5e'] }, { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [ '0xA78deb2Fa79463945C247991075E2a0e98Ba7A09', '0xe2DD09d719Da89e5a3D0F2549c7E24566e947260' @@ -75,7 +75,7 @@ const nftLevelCredentials: Credentials = { ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x02354A1F160A3fd7ac8b02ee91F04104440B28E7'] } ] @@ -84,13 +84,13 @@ const nftLevelCredentials: Credentials = { const serviceLevelCredentials: Credentials = { deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0xA78deb2Fa79463945C247991075E2a0e98Ba7A09'] } ], allow: [ { - type: 'address', + 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 2e7edf900..73817ff12 100644 --- a/src/test/unit/credentials.test.ts +++ b/src/test/unit/credentials.test.ts @@ -13,7 +13,7 @@ import { import { ENVIRONMENT_VARIABLES } from '../../utils/constants.js' import { homedir } from 'os' import { DEVELOPMENT_CHAIN_ID } from '../../utils/address.js' -import { Credentials } from '@oceanprotocol/ddo-js' +import { Credentials, CREDENTIALS_TYPES } from '@oceanprotocol/ddo-js' let envOverrides: OverrideEnvConfig[] @@ -53,7 +53,7 @@ describe('credentials', () => { allow: [], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ] @@ -69,6 +69,7 @@ describe('credentials', () => { allow: [], deny: [ { + // @ts-expect-error type: 'accessList', values: [consumerAddress] // not a valid SC address anyway } @@ -84,7 +85,7 @@ describe('credentials', () => { deny: [], allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ] @@ -98,7 +99,7 @@ describe('credentials', () => { deny: [], allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x123'] } ] @@ -112,7 +113,7 @@ describe('credentials', () => { allow: [], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x456'] } ] @@ -125,13 +126,13 @@ describe('credentials', () => { const credentials: Credentials = { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x123'] } ] @@ -144,13 +145,13 @@ describe('credentials', () => { const credentials: Credentials = { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x456'] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ] @@ -165,6 +166,7 @@ describe('credentials', () => { allow: [], deny: [ { + // @ts-expect-error type: 'unknow_type', values: ['0x456'] } @@ -176,17 +178,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'] } ] @@ -197,12 +199,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'] } @@ -217,13 +220,13 @@ describe('credentials', () => { credentials: { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['*'] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: ['0x2222', '0x333'] } ] @@ -239,13 +242,13 @@ describe('credentials', () => { const credentials: Credentials = { allow: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ], deny: [ { - type: 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [] } ] diff --git a/src/utils/credentials.ts b/src/utils/credentials.ts index 85a2a257a..9a5c7e2d4 100644 --- a/src/utils/credentials.ts +++ b/src/utils/credentials.ts @@ -4,10 +4,10 @@ import { AccessListContract } from '../@types/OceanNode.js' import { CORE_LOGGER } from './logging/common.js' import { Credential, - CREDENTIAL_TYPES, Credentials, + CREDENTIALS_TYPES, KNOWN_CREDENTIALS_TYPES -} from '../@types/DDO/Credentials.js' +} from '@oceanprotocol/ddo-js' import { getNFTContract } from '../components/Indexer/utils.js' import { isDefined } from './util.js' import { getConfiguration } from './config.js' @@ -38,7 +38,7 @@ export function isAddressCredentialMatch( credential: Credential, consumerCredentials: Credential ): boolean { - if (credential?.type?.toLowerCase() !== CREDENTIAL_TYPES.ADDRESS) { + if (credential?.type?.toLowerCase() !== CREDENTIALS_TYPES.ADDRESS) { return false } if (credential.values.length > 0) { @@ -50,7 +50,7 @@ export function isAddressCredentialMatch( } function isAddressMatchAll(credential: Credential): boolean { - if (credential?.type?.toLowerCase() !== CREDENTIAL_TYPES.ADDRESS) { + if (credential?.type?.toLowerCase() !== CREDENTIALS_TYPES.ADDRESS) { return false } if (credential.values.length > 0) { @@ -83,7 +83,7 @@ export async function checkCredentials( chainId?: number ): Promise { const consumerCredentials: Credential = { - type: CREDENTIAL_TYPES.ADDRESS, // 'address', + type: CREDENTIALS_TYPES.ADDRESS, values: [String(consumerAddress)?.toLowerCase()] } @@ -100,7 +100,7 @@ export async function checkCredentials( let denyCount = 0 for (const cred of credentials.deny) { const { type } = cred - if (type === CREDENTIAL_TYPES.ADDRESS) { + if (type === CREDENTIALS_TYPES.ADDRESS) { const accessDeny = isAddressCredentialMatch(cred, consumerCredentials) // credential is on deny list, so it should be blocked access if (accessDeny) { @@ -122,7 +122,7 @@ export async function checkCredentials( let matchCount = 0 for (const cred of credentials.allow) { const { type } = cred - if (type === CREDENTIAL_TYPES.ADDRESS) { + if (type === CREDENTIALS_TYPES.ADDRESS) { const accessAllow = isAddressCredentialMatch(cred, consumerCredentials) if (accessAllow || isAddressMatchAll(cred)) { // if no match_allow or 'any', its fine @@ -132,7 +132,7 @@ export async function checkCredentials( // otherwise, match 'all', in this case the amount of matches should be the same of the amount of rules matchCount++ } - } else if (type === CREDENTIAL_TYPES.ACCESS_LIST && chainId) { + } else if (type === CREDENTIALS_TYPES.ACCESS_LIST && chainId) { const config = await getConfiguration() const supportedNetwork = config.supportedNetworks[String(chainId)] if (supportedNetwork) { From 0de2b5c692730ffd8b4cb1d236226abb4451c7e2 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 23 Apr 2025 10:59:18 +0300 Subject: [PATCH 19/19] refactor match credentials --- src/test/unit/credentials.test.ts | 58 ++++++++- src/utils/credentials.ts | 204 +++++++++++++++--------------- 2 files changed, 161 insertions(+), 101 deletions(-) diff --git a/src/test/unit/credentials.test.ts b/src/test/unit/credentials.test.ts index 73817ff12..5be299e36 100644 --- a/src/test/unit/credentials.test.ts +++ b/src/test/unit/credentials.test.ts @@ -38,6 +38,7 @@ describe('credentials', () => { const accessGranted2 = await checkCredentials(credentialsEmapty, consumerAddress) expect(accessGranted2).to.equal(false) }) + 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 = { @@ -48,6 +49,7 @@ describe('credentials', () => { const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(false) }) + it('should deny access with empty values in deny lists', async () => { const credentials: Credentials = { allow: [], @@ -69,8 +71,7 @@ describe('credentials', () => { allow: [], deny: [ { - // @ts-expect-error - type: 'accessList', + type: CREDENTIALS_TYPES.ACCESS_LIST, values: [consumerAddress] // not a valid SC address anyway } ] @@ -94,6 +95,7 @@ describe('credentials', () => { const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(false) }) + it('should allow access with address in allow list', async () => { const credentials: Credentials = { deny: [], @@ -108,6 +110,7 @@ describe('credentials', () => { const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(true) }) + it('should deny access with address not explicitly in deny list but also without any allow list', async () => { const credentials: Credentials = { allow: [], @@ -122,6 +125,7 @@ describe('credentials', () => { 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', async () => { const credentials: Credentials = { allow: [ @@ -141,6 +145,7 @@ describe('credentials', () => { const accessGranted = await checkCredentials(credentials, consumerAddress) expect(accessGranted).to.equal(false) }) + it('should deny access with address not in allow list', async () => { const credentials: Credentials = { allow: [ @@ -262,6 +267,55 @@ describe('credentials', () => { 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/credentials.ts b/src/utils/credentials.ts index 9a5c7e2d4..e3747d8b8 100644 --- a/src/utils/credentials.ts +++ b/src/utils/credentials.ts @@ -15,51 +15,118 @@ import { getBlockchainHandler } from './blockchain.js' import { getOceanArtifactsAdressesByChainId } from './address.js' -export function findCredential( - credentials: Credential[], - consumerCredentials: Credential -): Credential | undefined { - 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]) - ) - } - } - return false - }) +function isValidCredentialsList(credentials: Credential[]): boolean { + return Array.isArray(credentials) && credentials.length > 0 } -export function isAddressCredentialMatch( +function isAddressCredentialMatch( credential: Credential, consumerCredentials: Credential ): boolean { if (credential?.type?.toLowerCase() !== CREDENTIALS_TYPES.ADDRESS) { return false } - if (credential.values.length > 0) { - const credentialValues = credential.values.map((v) => String(v)?.toLowerCase()) - return credentialValues.includes(consumerCredentials.values[0]) + + 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 } - if (credential.values.length > 0) { - const filteredValues: string[] = credential.values.filter((value: string) => { - return value?.toLowerCase() === '*' // address - }) - return filteredValues.length > 0 - } - return false + + return credential.values.some((value) => value?.toLowerCase() === '*') } export function hasAddressMatchAllRule(credentials: Credential[]): boolean { @@ -72,88 +139,27 @@ export function hasAddressMatchAllRule(credentials: Credential[]): boolean { return isDefined(creds) } -/** - * This method checks credentials - * @param credentials credentials - * @param consumerAddress consumer address - */ export async function checkCredentials( credentials: Credentials, consumerAddress: string, chainId?: number ): Promise { + if (!credentials || (!credentials?.allow && !credentials?.deny)) { + return false + } + const consumerCredentials: Credential = { type: CREDENTIALS_TYPES.ADDRESS, values: [String(consumerAddress)?.toLowerCase()] } - // if no address-based credentials are defined (both allow and deny lists are empty), access to the asset is restricted to everybody; - // to allow access to everybody, the symbol * will be used in the allow list; - // if a web3 address is present on both deny and allow lists, the deny list takes precedence - // and access to the asset is denied for the respective address. - const accessGranted = false - // check deny access - // https://github.com/oceanprotocol/ocean-node/issues/810 - // for deny rules: if value does not exist or it's empty -> there is no deny list. if value list has at least one element, check it - - if (Array.isArray(credentials?.deny) && credentials.deny.length > 0) { - let denyCount = 0 - for (const cred of credentials.deny) { - const { type } = cred - if (type === CREDENTIALS_TYPES.ADDRESS) { - const accessDeny = isAddressCredentialMatch(cred, consumerCredentials) - // credential is on deny list, so it should be blocked access - if (accessDeny) { - if (!isDefined(credentials.match_deny) || credentials.match_deny === 'any') { - return false - } - } - denyCount++ - // credential not found, so it really depends if we have a match on the allow list instead - } - } - if (credentials.match_deny === 'all' && denyCount === credentials.deny.length) { - return false - } - } - // check allow access - // for allow rules: if value does not exist or it's empty -> no one has access. if value list has at least one element, check it - if (Array.isArray(credentials?.allow) && credentials.allow.length > 0) { - let matchCount = 0 - for (const cred of credentials.allow) { - const { type } = cred - if (type === CREDENTIALS_TYPES.ADDRESS) { - const accessAllow = isAddressCredentialMatch(cred, consumerCredentials) - if (accessAllow || isAddressMatchAll(cred)) { - // if no match_allow or 'any', its fine - if (!isDefined(credentials.match_allow) || credentials.match_allow === 'any') { - return true - } - // otherwise, match 'all', in this case the amount of matches should be the same of the amount of rules - matchCount++ - } - } else if (type === CREDENTIALS_TYPES.ACCESS_LIST && chainId) { - const config = await getConfiguration() - const supportedNetwork = config.supportedNetworks[String(chainId)] - if (supportedNetwork) { - const blockChain = getBlockchainHandler(supportedNetwork) - for (const accessListContractAddress of cred.values) { - const balanceOk = await findAccessListCredentials( - blockChain.getSigner(), - accessListContractAddress, - consumerAddress - ) - if (balanceOk) return true - } - } - } - // extend function to ACCESS_LIST (https://github.com/oceanprotocol/ocean-node/issues/804) - } - if (credentials.match_allow === 'all' && matchCount === credentials.allow.length) { - 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 {