diff --git a/package-lock.json b/package-lock.json index ac8b1e806..9a48661d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocean-node", - "version": "3.2.2", + "version": "3.2.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocean-node", - "version": "3.2.2", + "version": "3.2.21", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index e47d63b1f..2f79e8a5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocean-node", - "version": "3.2.2", + "version": "3.2.21", "description": "Ocean Node is used to run all core services in the Ocean stack", "author": "Ocean Protocol Foundation", "license": "Apache-2.0", diff --git a/src/@types/OceanNode.ts b/src/@types/OceanNode.ts index 2270ba775..d6582fa7a 100644 --- a/src/@types/OceanNode.ts +++ b/src/@types/OceanNode.ts @@ -143,6 +143,7 @@ export interface OceanNodeConfig { httpCertPath?: string httpKeyPath?: string enableBenchmark?: boolean + isPSConfigured: boolean persistentStorage?: PersistentStorageConfig } @@ -185,6 +186,7 @@ export interface OceanNodeStatus { version: string http: boolean p2p: boolean + isPSConfigured: boolean provider: OceanNodeProvider[] indexer: OceanNodeIndexer[] escrowAddress: AddressPerChain diff --git a/src/OceanNode.ts b/src/OceanNode.ts index 4e23fedbe..8b0b6735d 100644 --- a/src/OceanNode.ts +++ b/src/OceanNode.ts @@ -23,6 +23,7 @@ import { createPersistentStorage } from './components/persistentStorage/createPe import { PersistentStorageFactory } from './components/persistentStorage/PersistentStorageFactory.js' import { isAddress, FallbackProvider, ethers } from 'ethers' import { create256Hash } from './utils/crypt.js' +import { isPolicyServerConfigured } from './utils/config.js' export interface RequestLimiter { requester: string | string[] // IP address or peer ID @@ -255,6 +256,7 @@ export class OceanNode { } public getConfig(): OceanNodeConfig { + this.config.isPSConfigured = isPolicyServerConfigured() return this.config } diff --git a/src/components/core/handler/downloadHandler.ts b/src/components/core/handler/downloadHandler.ts index 4c97b6b1b..009a7261f 100644 --- a/src/components/core/handler/downloadHandler.ts +++ b/src/components/core/handler/downloadHandler.ts @@ -313,6 +313,11 @@ export class DownloadHandler extends CommandHandler { if (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. + CORE_LOGGER.logMessage('Checking DDO level credentials', true) + CORE_LOGGER.logMessage( + 'isPolicyServerConfigured: ' + isPolicyServerConfigured(), + true + ) if (isPolicyServerConfigured()) { const response = await policyServer.checkDownload( ddoInstance.getDid(), @@ -321,14 +326,24 @@ export class DownloadHandler extends CommandHandler { task.consumerAddress, task.policyServer ) + CORE_LOGGER.logMessage( + 'Policy server response: ' + JSON.stringify(response), + true + ) accessGrantedDDOLevel = response.success } else { + CORE_LOGGER.logMessage('Checking DDO level credentials INTERNAL', true) + CORE_LOGGER.logMessage( + 'isPolicyServerConfigured: ' + isPolicyServerConfigured(), + true + ) accessGrantedDDOLevel = await checkCredentials( task.consumerAddress, credentials as Credentials, await blockchain.getSigner() ) } + CORE_LOGGER.logMessage('DDO level credentials check complete', true) if (!accessGrantedDDOLevel) { CORE_LOGGER.logMessage( `Error: Access to asset ${ddoInstance.getDid()} was denied`, diff --git a/src/components/core/handler/feesHandler.ts b/src/components/core/handler/feesHandler.ts index 0dbd44292..015134411 100644 --- a/src/components/core/handler/feesHandler.ts +++ b/src/components/core/handler/feesHandler.ts @@ -16,7 +16,10 @@ import { ProviderInitialize } from '../../../@types/Fees.js' import { getNonce } from '../utils/nonceHandler.js' import { streamToString } from '../../../utils/util.js' import { isOrderingAllowedForAsset } from './downloadHandler.js' -import { DDOManager } from '@oceanprotocol/ddo-js' +import { Credentials, DDOManager } from '@oceanprotocol/ddo-js' +import { checkCredentials } from '../../../utils/credentials.js' +import { isPolicyServerConfigured } from '../../../utils/index.js' +import { PolicyServer } from '../../policyServer/index.js' export class FeesHandler extends CommandHandler { validate(command: GetFeesCommand): ValidateParams { @@ -60,14 +63,164 @@ export class FeesHandler extends CommandHandler { } } const ddoInstance = DDOManager.getDDOClass(ddo) - const { services } = ddoInstance.getDDOFields() as any + const { + chainId: ddoChainId, + credentials, + services + } = ddoInstance.getDDOFields() as any const service = services.find((what: any) => what.id === task.serviceId) if (!service) { errorMsg = 'Invalid serviceId' } - if (service.type === 'compute') { + if (service?.type === 'compute') { errorMsg = 'Use the initializeCompute endpoint to initialize compute jobs' } + + if (errorMsg) { + PROVIDER_LOGGER.logMessageWithEmoji( + errorMsg, + true, + GENERIC_EMOJIS.EMOJI_CROSS_MARK, + LOG_LEVELS_STR.LEVEL_ERROR + ) + return { + stream: null, + status: { + httpStatus: 500, + error: errorMsg + } + } + } + + const policyServer = new PolicyServer() + if (credentials || service.credentials) { + if (!task.consumerAddress) { + return { + stream: null, + status: { + httpStatus: 400, + error: 'consumerAddress is required to check credentials' + } + } + } + + let signer + if (!isPolicyServerConfigured()) { + const oceanNode = this.getOceanNode() + const supportedNetwork = oceanNode.getConfig().supportedNetworks?.[ddoChainId] + if (!supportedNetwork) { + return { + stream: null, + status: { + httpStatus: 400, + error: `Fees handler: Unsupported chain id ${ddoChainId}` + } + } + } + + const blockchain = oceanNode.getBlockchain(supportedNetwork.chainId) + if (!blockchain) { + return { + stream: null, + status: { + httpStatus: 400, + error: `Fees handler: Blockchain instance not available for chain ${supportedNetwork.chainId}` + } + } + } + + const { ready, error } = await blockchain.isNetworkReady() + if (!ready) { + return { + stream: null, + status: { + httpStatus: 400, + error: `Fees handler: ${error}` + } + } + } + signer = await blockchain.getSigner() + } + + let accessGrantedDDOLevel = false + PROVIDER_LOGGER.info( + `Checking credentials for consumer address: ${task.consumerAddress} AND credentials: ${JSON.stringify(credentials)}` + ) + if (credentials) { + if (isPolicyServerConfigured()) { + PROVIDER_LOGGER.info( + `Checking credentials for consumer address on policy server: ${task.consumerAddress}` + ) + const response = await policyServer.initializePSVerification( + ddoInstance.getDid(), + ddo, + task.serviceId, + task.consumerAddress, + task.policyServer + ) + accessGrantedDDOLevel = response.success + } else { + PROVIDER_LOGGER.info( + `Checking credentials for consumer address internal: ${task.consumerAddress}` + ) + accessGrantedDDOLevel = await checkCredentials( + task.consumerAddress, + credentials as Credentials, + signer + ) + } + } + + PROVIDER_LOGGER.info(`credentials: ${JSON.stringify(credentials)}`) + PROVIDER_LOGGER.info(`accessGrantedDDOLevel: ${accessGrantedDDOLevel}`) + + if (credentials && !accessGrantedDDOLevel) { + const error = `Error: Access to asset ${ddoInstance.getDid()} was denied` + PROVIDER_LOGGER.error(error) + return { + stream: null, + status: { httpStatus: 403, error } + } + } + + if (service.credentials) { + let accessGrantedServiceLevel: boolean + if (isPolicyServerConfigured()) { + const response = await policyServer.initializePSVerification( + ddoInstance.getDid(), + ddo, + service.id, + task.consumerAddress, + task.policyServer + ) + accessGrantedServiceLevel = accessGrantedDDOLevel || response.success + } else { + PROVIDER_LOGGER.info( + 'Checking credentials for consumer address internal for service: ' + + task.consumerAddress + ) + accessGrantedServiceLevel = await checkCredentials( + task.consumerAddress, + service.credentials, + signer + ) + } + PROVIDER_LOGGER.info( + `service.credentials: ${JSON.stringify(service.credentials)}` + ) + PROVIDER_LOGGER.info(`accessGrantedServiceLevel: ${accessGrantedServiceLevel}`) + + if (!accessGrantedServiceLevel) { + const error = `Error: Access to service with id ${service.id} was denied` + PROVIDER_LOGGER.error(error) + return { + stream: null, + status: { httpStatus: 403, error } + } + } + } + } + const now = new Date().getTime() / 1000 let validUntil = service.timeout === 0 ? 0 : now + service.timeout // first, make it service default if (task.validUntil && !isNaN(task.validUntil)) { diff --git a/src/components/core/utils/nonceHandler.ts b/src/components/core/utils/nonceHandler.ts index 44ecdea5c..59884c498 100644 --- a/src/components/core/utils/nonceHandler.ts +++ b/src/components/core/utils/nonceHandler.ts @@ -200,16 +200,23 @@ async function validateNonceAndSignature( ['bytes'], [ethers.hexlify(ethers.toUtf8Bytes(message))] ) - const messageHashBytes = ethers.toBeArray(consumerMessage) + const messageHashBytes = ethers.getBytes(consumerMessage) + const legacyMessageHashBytes = ethers.toBeArray(consumerMessage) // Try EOA signature validation try { const addressFromHashSignature = ethers.verifyMessage(consumerMessage, signature) const addressFromBytesSignature = ethers.verifyMessage(messageHashBytes, signature) + const addressFromLegacyBytesSignature = ethers.verifyMessage( + legacyMessageHashBytes, + signature + ) if ( ethers.getAddress(addressFromHashSignature)?.toLowerCase() === ethers.getAddress(consumer)?.toLowerCase() || ethers.getAddress(addressFromBytesSignature)?.toLowerCase() === + ethers.getAddress(consumer)?.toLowerCase() || + ethers.getAddress(addressFromLegacyBytesSignature)?.toLowerCase() === ethers.getAddress(consumer)?.toLowerCase() ) { return { valid: true } diff --git a/src/components/core/utils/statusHandler.ts b/src/components/core/utils/statusHandler.ts index 2e0d667fd..432dfd627 100644 --- a/src/components/core/utils/statusHandler.ts +++ b/src/components/core/utils/statusHandler.ts @@ -128,6 +128,7 @@ export async function status( version: getPackageVersion(), http: config.hasHttp, p2p: config.hasP2P, + isPSConfigured: config.isPSConfigured, provider: [], indexer: [], escrowAddress: {}, @@ -152,6 +153,7 @@ export async function status( nodeStatus.platform.freemem = os.freemem() nodeStatus.platform.loadavg = os.loadavg() nodeStatus.uptime = process.uptime() + nodeStatus.isPSConfigured = config.isPSConfigured // depends on request if (detailed) { diff --git a/src/test/integration/credentials.test.ts b/src/test/integration/credentials.test.ts index b2e2fed48..e150cf183 100644 --- a/src/test/integration/credentials.test.ts +++ b/src/test/integration/credentials.test.ts @@ -31,6 +31,7 @@ import { } from '../../utils/index.js' import { DownloadHandler } from '../../components/core/handler/downloadHandler.js' import { GetDdoHandler } from '../../components/core/handler/ddoHandler.js' +import { ProviderFees } from '../../components/core/utils/feesHandler.js' import { Readable } from 'stream' import { OceanNodeConfig } from '../../@types/OceanNode.js' @@ -306,6 +307,21 @@ describe('********** [Credentials Flow] - Should run a complete node flo it('should start an order for all consumers', async function () { this.timeout(DEFAULT_TEST_TIMEOUT * 3) + const providerFeesFactory = new ProviderFees(oceanNode) + const createTestProviderFees = async (asset: any) => { + const service = asset.services[0] + const now = Date.now() / 1000 + const validUntil = service.timeout === 0 ? 0 : now + service.timeout + const providerFees = await providerFeesFactory.createProviderFee( + asset, + service, + validUntil + ) + assert(providerFees, 'provider fees could not be created') + return providerFees + } + + const providerFees = await createTestProviderFees(ddo) for (let i = 0; i < 3; i++) { const orderTxReceipt = await orderAsset( ddo, @@ -313,13 +329,16 @@ describe('********** [Credentials Flow] - Should run a complete node flo consumerAccounts[i], consumerAddresses[i], publisherAccount, - oceanNode + oceanNode, + providerFees ) assert(orderTxReceipt, `order transaction for consumer ${i} failed`) const txHash = orderTxReceipt.hash assert(txHash, `transaction id not found for consumer ${i}`) orderTxIds.push(txHash) } + + const providerFeesWithMatchAll = await createTestProviderFees(ddoWithMatchAll) for (let i = 0; i < 3; i++) { const orderTxReceipt = await orderAsset( ddoWithMatchAll, @@ -327,7 +346,8 @@ describe('********** [Credentials Flow] - Should run a complete node flo consumerAccounts[i], consumerAddresses[i], publisherAccount, - oceanNode + oceanNode, + providerFeesWithMatchAll ) assert(orderTxReceipt, `order transaction for consumer ${i} failed`) const txHash = orderTxReceipt.hash diff --git a/src/utils/config/builder.ts b/src/utils/config/builder.ts index 0fc042b86..11f808ac5 100644 --- a/src/utils/config/builder.ts +++ b/src/utils/config/builder.ts @@ -20,6 +20,7 @@ import { CONFIG_LOGGER } from '../logging/common.js' import { LOG_LEVELS_STR, GENERIC_EMOJIS } from '../logging/Logger.js' import { OceanNodeConfigSchema } from './schemas.js' import { ENV_TO_CONFIG_MAPPING } from './constants.js' +import { isPolicyServerConfigured } from '../config.js' import { fileURLToPath } from 'url' import lodash from 'lodash' @@ -281,6 +282,7 @@ export function buildMergedConfig(): OceanNodeConfig { config.c2dClusters = buildC2DClusters( config.dockerComputeEnvironments as C2DDockerConfig[] ) + config.isPSConfigured = isPolicyServerConfigured() return config as OceanNodeConfig } diff --git a/src/utils/credentials.ts b/src/utils/credentials.ts index b21c25f21..871a109cd 100644 --- a/src/utils/credentials.ts +++ b/src/utils/credentials.ts @@ -52,7 +52,6 @@ export async function checkCredentials( matchDeny, 'deny' ) - // If evaluation says to deny, return false if (denyResult.shouldDeny) { CORE_LOGGER.logMessage( @@ -73,7 +72,6 @@ export async function checkCredentials( matchAllow, 'allow' ) - return allowResult.shouldAllow } @@ -163,6 +161,9 @@ export async function checkSingleCredential( consumerAddress: string, signer: Signer | null ): Promise { + if (!credential || typeof credential.type !== 'string') { + return null + } const credentialType = credential.type.toLowerCase() // ======================================== @@ -172,20 +173,34 @@ export async function checkSingleCredential( // Type assertion since we know it's address type const addressCredential = credential as any - if (!isDefined(addressCredential.values) || addressCredential.values.length === 0) { + if ( + !Array.isArray(addressCredential.values) || + addressCredential.values.length === 0 + ) { return false } - // Check for wildcard (*) - const hasWildcard = addressCredential.values.some( - (value: string) => value.toLowerCase() === '*' + const normalizedValues = addressCredential.values.flatMap( + (value: unknown): string[] => { + if (typeof value === 'string') return [value.toLowerCase()] + if ( + value && + typeof value === 'object' && + 'address' in value && + typeof value.address === 'string' + ) { + return [value.address.toLowerCase()] + } + return [] + } ) - if (hasWildcard) { + + // Check for wildcard (*) + if (normalizedValues.includes('*')) { return true } // Check if address is in the list - const normalizedValues = addressCredential.values.map((v: string) => v.toLowerCase()) return normalizedValues.includes(consumerAddress.toLowerCase()) }