diff --git a/src/ddo/DDO.ts b/src/ddo/DDO.ts index 815ee9f0..ea30d60e 100644 --- a/src/ddo/DDO.ts +++ b/src/ddo/DDO.ts @@ -7,6 +7,7 @@ import Web3Provider from '../datatokens/Web3Provider' import { BestPrice } from './interfaces/BestPrice' import { DataTokenInfo } from './interfaces/DataTokenInfo' import { PurgatoryData } from './interfaces/PurgatoryData' +import { Credentials } from './interfaces/Credentials' /** * DID Descriptor Object. * Contains all the data related to an asset. @@ -62,6 +63,8 @@ export class DDO { public dataTokenInfo?: DataTokenInfo + public credentials?: Credentials + public constructor(ddo: Partial = {}) { Object.assign(this, ddo, { created: (ddo && ddo.created) || new Date().toISOString().replace(/\.[0-9]{3}/, '') diff --git a/src/ddo/interfaces/Credentials.ts b/src/ddo/interfaces/Credentials.ts new file mode 100644 index 00000000..d8422350 --- /dev/null +++ b/src/ddo/interfaces/Credentials.ts @@ -0,0 +1,16 @@ +export enum CredentialType { + address = 'address', + credential3Box = 'credential3Box' +} + +export type CredentialAction = 'allow' | 'deny' + +export interface Credential { + type: CredentialType + value: string[] +} + +export interface Credentials { + allow?: Credential[] + deny?: Credential[] +} diff --git a/src/ddo/interfaces/index.ts b/src/ddo/interfaces/index.ts index 385f3da5..0c5d8c24 100644 --- a/src/ddo/interfaces/index.ts +++ b/src/ddo/interfaces/index.ts @@ -13,3 +13,4 @@ export * from './PublicKey' export * from './Service' export * from './ServicePrices' export * from './PurgatoryData' +export * from './Credentials' diff --git a/src/ocean/Assets.ts b/src/ocean/Assets.ts index a2fc55f8..bf50853b 100644 --- a/src/ocean/Assets.ts +++ b/src/ocean/Assets.ts @@ -13,6 +13,12 @@ import { Provider } from '../provider/Provider' import { isAddress } from 'web3-utils' import { MetadataMain } from '../ddo/interfaces' import { TransactionReceipt } from 'web3-core' +import { + CredentialType, + CredentialAction, + Credentials +} from '../ddo/interfaces/Credentials' +import { updateCredentialDetail, removeCredentialDetail } from './AssetsCredential' import { Consumable } from '../ddo/interfaces/Consumable' export enum CreateProgressStep { @@ -285,6 +291,63 @@ export class Assets extends Instantiable { return ddo } + /** + * Update Credentials attribute in DDO + * @param {ddo} DDO + * @param {credentialType} CredentialType e.g. address / credentail3Box + * @param {allowList} string[] List of allow credential + * @param {denyList} string[] List of deny credential + * @return {Promise} Updated DDO + */ + public async updateCredentials( + ddo: DDO, + credentialType: CredentialType, + allowList: string[], + denyList: string[] + ): Promise { + if (allowList && allowList.length > 0) { + ddo = updateCredentialDetail(ddo, credentialType, allowList, 'allow') + } else { + ddo = removeCredentialDetail(ddo, credentialType, 'allow') + } + + if (denyList && denyList.length > 0) { + ddo = updateCredentialDetail(ddo, credentialType, denyList, 'deny') + } else { + ddo = removeCredentialDetail(ddo, credentialType, 'deny') + } + return ddo + } + + /** + * check if a credential can consume a dataset + * @param {ddo} DDO + * @param {credentialType} CredentialType e.g. address / credentail3Box + * @param {value} string credential + * @return {boolean} allowed ? + */ + public checkCredential( + ddo: DDO, + credentialType: CredentialType, + value: string + ): boolean { + let allowed = true + if (!ddo.credentials) return allowed + if (ddo.credentials.allow && ddo.credentials.allow.length > 0) { + const allowList = ddo.credentials.allow.find( + (credentail) => credentail.type === credentialType + ) + if (allowList && !allowList.value.includes(value)) allowed = false + } + if (ddo.credentials.deny && ddo.credentials.deny.length > 0) { + const denyList = ddo.credentials.deny.find( + (credentail) => credentail.type === credentialType + ) + if (denyList && denyList.value.includes(value)) allowed = false + } + return allowed + } + /** * Publish DDO on chain. * @param {ddo} DDO diff --git a/src/ocean/AssetsCredential.ts b/src/ocean/AssetsCredential.ts new file mode 100644 index 00000000..fdf5a34d --- /dev/null +++ b/src/ocean/AssetsCredential.ts @@ -0,0 +1,169 @@ +import { DDO } from '../ddo/DDO' +import { + Credentials, + Credential, + CredentialType, + CredentialAction +} from '../ddo/interfaces/Credentials' + +/** + * checks if a credential list exists for a specific action + * @param {credentials} Credentials list of crentials from ddo + * @param {credentialType} CredentialType e.g. address / credential3Box + * @param {credentialAction} CredentialAction allow or deny + * @return {boolean} + */ +export function checkCredentialExist( + credentials: Credentials, + credentialType: CredentialType, + credentialAction: CredentialAction +): boolean { + let isExist = false + if (credentialAction === 'allow') { + if (credentials && credentials.allow) { + const allowList = credentials.allow.find( + (credential) => credential.type === credentialType + ) + isExist = allowList && allowList.value.length > 0 + } + return isExist + } else { + if (credentials && credentials.deny) { + const dennyList = credentials.deny.find( + (credential) => credential.type === credentialType + ) + isExist = dennyList && dennyList.value.length > 0 + } + return isExist + } +} + +/** + * Removes all credentials of a certain type for a specific action + * @param {ddo} DDO + * @param {credentialType} CredentialType e.g. address / credential3Box + * @param {credentialAction} CredentialAction allow or deny + * @return {DDO} + */ +export function removeCredentialDetail( + ddo: DDO, + credentialType: CredentialType, + credentialAction: CredentialAction +): DDO { + const exists = this.checkCredentialExist( + ddo.credentials, + credentialType, + credentialAction + ) + if (credentialAction === 'allow') { + if (exists) { + ddo.credentials.allow = ddo.credentials.allow.filter( + (credential) => credential.type !== credentialType + ) + } + if (!ddo.credentials.allow) { + ddo.credentials = { + deny: ddo.credentials.deny + } + } + } else { + if (exists) { + ddo.credentials.deny = ddo.credentials.deny.filter( + (credential) => credential.type !== credentialType + ) + } + if (!ddo.credentials.deny) { + ddo.credentials = { + allow: ddo.credentials.allow + } + } + } + return ddo +} + +/** + * Updates credentials of a certain type for a specific action + * @param {ddo} DDO + * @param {credentialType} CredentialType e.g. address / credential3Box + * @param {list} string[] list of values + * @param {credentialAction} CredentialAction allow or deny + * @return {DDO} + */ + +export function updateCredentialDetail( + ddo: DDO, + credentialType: CredentialType, + list: string[], + credentialAction: CredentialAction +): DDO { + const exists = this.checkCredentialExist( + ddo.credentials, + credentialType, + credentialAction + ) + if (credentialAction === 'allow') { + if (exists) { + ddo.credentials.allow.find((credential) => { + if (credential.type === credentialType) { + credential.value = list + } + }) + } else { + return this.addCredentialDetail(ddo, credentialType, list, credentialAction) + } + } else { + if (exists) { + ddo.credentials.deny.find((credential) => { + if (credential.type === credentialType) { + credential.value = list + } + }) + } else { + return this.addCredentialDetail(ddo, credentialType, list, credentialAction) + } + } + return ddo +} + +/** + * Adds values to credentials of a certain type for a specific action + * @param {ddo} DDO + * @param {credentialType} CredentialType e.g. address / credential3Box + * @param {list} string[] list of values + * @param {credentialAction} CredentialAction allow or deny + * @return {DDO} + */ + +export function addCredentialDetail( + ddo: DDO, + credentialType: CredentialType, + list: string[], + credentialAction: CredentialAction +) { + const newCredentialDetail: Credential = { + type: credentialType, + value: list + } + if (credentialAction === 'allow') { + if (ddo.credentials && ddo.credentials.allow) { + ddo.credentials.allow.push(newCredentialDetail) + } else { + const newCredentials: Credentials = { + allow: [newCredentialDetail], + deny: ddo.credentials && ddo.credentials.deny + } + ddo.credentials = newCredentials + } + } else { + if (ddo.credentials && ddo.credentials.deny) { + ddo.credentials.deny.push(newCredentialDetail) + } else { + const newCredential: Credentials = { + allow: ddo.credentials && ddo.credentials.allow, + deny: [newCredentialDetail] + } + ddo.credentials = newCredential + } + } + return ddo +} diff --git a/test/integration/Marketplaceflow.test.ts b/test/integration/Marketplaceflow.test.ts index be2302d5..080c9911 100644 --- a/test/integration/Marketplaceflow.test.ts +++ b/test/integration/Marketplaceflow.test.ts @@ -5,7 +5,14 @@ import spies from 'chai-spies' import Web3 from 'web3' import { AbiItem } from 'web3-utils/types' import { DataTokens } from '../../src/datatokens/Datatokens' -import { Account, EditableMetadata, Service, ServiceAccess, DID } from '../../src/lib' +import { + Account, + EditableMetadata, + Service, + ServiceAccess, + DID, + CredentialType +} from '../../src/lib' import { noDidPrefixed } from '../../src/utils/' import { Ocean } from '../../src/ocean/Ocean' import { ConfigHelper } from '../../src/utils/ConfigHelper' diff --git a/test/unit/ocean/Assets.test.ts b/test/unit/ocean/Assets.test.ts index 332c144a..99e70a61 100644 --- a/test/unit/ocean/Assets.test.ts +++ b/test/unit/ocean/Assets.test.ts @@ -3,19 +3,40 @@ import spies from 'chai-spies' import { SearchQuery, MetadataCache } from '../../../src/metadatacache/MetadataCache' import { Ocean } from '../../../src/ocean/Ocean' -import config from '../config' -import { DDO } from '../../../src/lib' +import Web3 from 'web3' +import { Account, DDO, CredentialType, ConfigHelper } from '../../../src/lib' import { responsify, getSearchResults } from '../helpers' - +const web3 = new Web3('http://127.0.0.1:8545') use(spies) describe('Assets', () => { let ocean: Ocean let metadataCache: MetadataCache - + let asset: any + let owner: Account + let alice: Account + let bob: Account + let charlie: Account + let ddo: DDO + let ddoWithAddressAnd3Box: DDO + const addressType = CredentialType.address + const threeBoxType = CredentialType.credential3Box + let walletA: string + let walletB: string + let walletC: string + const threeBoxValue = 'did:3:bafyre' beforeEach(async () => { + const config = new ConfigHelper().getConfig('development') + config.web3Provider = web3 ocean = await Ocean.getInstance(config) metadataCache = ocean.metadataCache // eslint-disable-line prefer-destructuring + owner = (await ocean.accounts.list())[0] + alice = (await ocean.accounts.list())[1] + walletA = alice.getId() + bob = (await ocean.accounts.list())[2] + walletB = bob.getId() + charlie = (await ocean.accounts.list())[3] + walletC = charlie.getId() }) afterEach(() => { @@ -57,4 +78,125 @@ describe('Assets', () => { assert.isDefined(assets.results[0].findServiceById) }) }) + it('Generates metadata', async () => { + asset = { + main: { + type: 'dataset', + name: 'test-dataset', + dateCreated: new Date(Date.now()).toISOString().split('.')[0] + 'Z', // remove milliseconds + author: 'oceanprotocol-team', + license: 'MIT', + files: [ + { + url: 'https://s3.amazonaws.com/testfiles.oceanprotocol.com/info.0.json', + checksum: 'efb2c764274b745f5fc37f97c6b0e761', + contentLength: '4535431', + contentType: 'text/csv', + encoding: 'UTF-8', + compression: 'zip' + } + ] + } + } + }) + + it('Alice creates a DDO', async () => { + const price = '10' // in datatoken + const publishedDate = new Date(Date.now()).toISOString().split('.')[0] + 'Z' + const timeout = 0 + const service1 = await ocean.assets.createAccessServiceAttributes( + alice, + price, + publishedDate, + timeout + ) + ddo = await ocean.assets.create(asset, alice, [service1], null) + }) + + it('should add allow credential', async () => { + assert(ocean.assets.checkCredential(ddo, addressType, walletA) === true) + assert(ocean.assets.checkCredential(ddo, addressType, walletC) === true) + const allowWalletAddressList = [walletA, walletB] + console.error(JSON.stringify(ddo.credentials)) + const newDdo = await ocean.assets.updateCredentials( + ddo, + addressType, + allowWalletAddressList, + [] + ) + assert(newDdo.credentials.allow.length === 1) + assert(ocean.assets.checkCredential(ddo, addressType, walletA) === true) + assert(ocean.assets.checkCredential(ddo, addressType, walletC) === false) + }) + + it('should append allow credential', async () => { + const allowWalletAddressList = [walletA, walletB] + const allow3BoxList = [threeBoxValue] + ddoWithAddressAnd3Box = await ocean.assets.updateCredentials( + ddo, + addressType, + allowWalletAddressList, + [] + ) + ddoWithAddressAnd3Box = await ocean.assets.updateCredentials( + ddo, + threeBoxType, + allow3BoxList, + [] + ) + assert(ddoWithAddressAnd3Box.credentials.allow.length === 2) + }) + + it('should add deny credential', async () => { + const denyWalletAddressList = [walletC] + const newDdo = await ocean.assets.updateCredentials( + ddo, + addressType, + [], + denyWalletAddressList + ) + assert(newDdo.credentials.deny.length === 1) + assert(ocean.assets.checkCredential(ddo, addressType, walletA) === true) + assert(ocean.assets.checkCredential(ddo, addressType, walletC) === false) + }) + + it('should append deny credential', async () => { + const denyWalletAddressList = [walletC] + const deny3BoxList = [threeBoxValue] + let newDdo = await ocean.assets.updateCredentials( + ddo, + addressType, + [], + denyWalletAddressList + ) + newDdo = await ocean.assets.updateCredentials(ddo, threeBoxType, [], deny3BoxList) + assert(newDdo.credentials.deny.length === 2) + }) + + it('should only remove allow credential by credential type', async () => { + const allowWalletAddressList = [walletA, walletB] + const allow3BoxList = [threeBoxValue] + let newDdo = await ocean.assets.updateCredentials( + ddo, + addressType, + allowWalletAddressList, + [] + ) + newDdo = await ocean.assets.updateCredentials( + ddoWithAddressAnd3Box, + threeBoxType, + allow3BoxList, + [] + ) + assert(newDdo.credentials.allow.length === 2) + newDdo = await ocean.assets.updateCredentials( + ddoWithAddressAnd3Box, + threeBoxType, + [], + [] + ) + assert(newDdo.credentials.allow.length === 1) + assert(ocean.assets.checkCredential(ddo, addressType, walletA) === true) + assert(ocean.assets.checkCredential(ddo, addressType, walletC) === false) + }) })