From 49ef0e099d2e34fedc071475fefa40e279218b3f Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Sun, 26 Feb 2023 16:44:35 -0500 Subject: [PATCH 01/19] feat: endpoint and noop world name validators --- .env.default | 15 ++- ...-name-checker.ts => world-name-checker.ts} | 39 +++++++- src/components.ts | 16 ++- ...ker.spec.ts => world-name-checker.spec.ts} | 99 ++++++++++++++++++- 4 files changed, 159 insertions(+), 10 deletions(-) rename src/adapters/{dcl-name-checker.ts => world-name-checker.ts} (73%) rename test/unit/{dcl-name-checker.spec.ts => world-name-checker.spec.ts} (54%) diff --git a/.env.default b/.env.default index 64b1236c..0dba6596 100644 --- a/.env.default +++ b/.env.default @@ -38,11 +38,22 @@ COMMS_FIXED_ADAPTER=ws-room:ws-room-service.decentraland.org/rooms/test-scene #LIVEKIT_API_KEY= #LIVEKIT_API_SECRET= +############################# +## Name permission checker ## +############################# +NAME_VALIDATOR=DCL_NAME_CHECKER + +#NAME_VALIDATOR=ENDPOINT_NAME_CHECKER (checks permissions against a a custom endpoint) +#ENDPOINT_NAME_CHECKER_BASE_URL=https://my-custom-name-checker.com/checker +#NAME_VALIDATOR=NOOP_NAME_CHECKER (allows deployment to any valid world name by any valid ETH address) + +########################## +## Miscelaneous configs ## +########################## + # number of ms that the deployment has to be newer than DEPLOYMENT_TTL=300000 MAX_PARCELS=4 MAX_SIZE=100 ALLOW_SDK6=false WHITELIST_URL=https://config.decentraland.org/worlds-whitelist.json -NAME_VALIDATOR=DCL_NAME_CHECKER -#NAME_VALIDATOR=ON_CHAIN_DCL_NAME_CHECKER (no need for thegraph but needs an ethereum node) diff --git a/src/adapters/dcl-name-checker.ts b/src/adapters/world-name-checker.ts similarity index 73% rename from src/adapters/dcl-name-checker.ts rename to src/adapters/world-name-checker.ts index 11b1d858..619559f6 100644 --- a/src/adapters/dcl-name-checker.ts +++ b/src/adapters/world-name-checker.ts @@ -8,7 +8,7 @@ type NamesResponse = { nfts: { name: string; owner: { id: string } }[] } -export const createDclNameChecker = ( +export const createTheGraphDclNameChecker = ( components: Pick ): IWorldNamePermissionChecker => { const logger = components.logs.getLogger('check-permissions') @@ -97,3 +97,40 @@ export const createOnChainDclNameChecker = async ( checkPermission } } + +export const createEndpointNameChecker = async ( + components: Pick +): Promise => { + const logger = components.logs.getLogger('check-permissions') + logger.info('Using Endpoint NameChecker') + + const nameCheckUrl = await components.config.requireString('ENDPOINT_NAME_CHECKER_BASE_URL') + + return { + checkPermission: async (ethAddress: EthAddress, worldName: string): Promise => { + if (worldName.length === 0 || ethAddress.length === 0) { + return false + } + return ( + (await components.fetch + .fetch(nameCheckUrl, { + method: 'POST' + }) + .then((response) => response.json())) === true + ) + } + } +} + +export const createNoOpNameChecker = async ( + components: Pick +): Promise => { + const logger = components.logs.getLogger('check-permissions') + logger.info('Using NoOp NameChecker') + + return { + checkPermission: async (ethAddress: EthAddress, worldName: string): Promise => { + return !(worldName.length === 0 || ethAddress.length === 0) + } + } +} diff --git a/src/components.ts b/src/components.ts index 4f82f88f..354b8547 100644 --- a/src/components.ts +++ b/src/components.ts @@ -15,20 +15,29 @@ import { } from '@dcl/catalyst-storage' import { createStatusComponent } from './adapters/status' import { createValidator } from './adapters/validator' -import { createDclNameChecker, createOnChainDclNameChecker } from './adapters/dcl-name-checker' +import { + createTheGraphDclNameChecker, + createOnChainDclNameChecker, + createNoOpNameChecker, + createEndpointNameChecker +} from './adapters/world-name-checker' import { createLimitsManagerComponent } from './adapters/limits-manager' import { createWorldsManagerComponent } from './adapters/worlds-manager' import { createCommsAdapterComponent } from './adapters/comms-adapter' async function determineNameValidator( - components: Pick + components: Pick ) { const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR') switch (nameValidatorStrategy) { case 'DCL_NAME_CHECKER': - return createDclNameChecker(components) + return createTheGraphDclNameChecker(components) case 'ON_CHAIN_DCL_NAME_CHECKER': return await createOnChainDclNameChecker(components) + case 'ENDPOINT_NAME_CHECKER': + return await createEndpointNameChecker(components) + case 'NOOP_NAME_CHECKER': + return await createNoOpNameChecker(components) // Add more name validator strategies as needed here } @@ -82,6 +91,7 @@ export async function initComponents(): Promise { const namePermissionChecker: IWorldNamePermissionChecker = await determineNameValidator({ config, ethereumProvider, + fetch, logs, marketplaceSubGraph }) diff --git a/test/unit/dcl-name-checker.spec.ts b/test/unit/world-name-checker.spec.ts similarity index 54% rename from test/unit/dcl-name-checker.spec.ts rename to test/unit/world-name-checker.spec.ts index 8ef69913..8987a5f0 100644 --- a/test/unit/dcl-name-checker.spec.ts +++ b/test/unit/world-name-checker.spec.ts @@ -1,10 +1,18 @@ import { createConfigComponent } from '@well-known-components/env-config-provider' import { Variables } from '@well-known-components/thegraph-component/dist/types' -import { createDclNameChecker, createOnChainDclNameChecker } from '../../src/adapters/dcl-name-checker' +import { + createEndpointNameChecker, + createNoOpNameChecker, + createOnChainDclNameChecker, + createTheGraphDclNameChecker +} from '../../src/adapters/world-name-checker' import { createLogComponent } from '@well-known-components/logger' import { IConfigComponent, ILoggerComponent } from '@well-known-components/interfaces' import { getIdentity } from '../utils' import { createHttpProviderMock } from '../mocks/http-provider-mock' +import { IWorldNamePermissionChecker } from '../../src/types' +import { IFetchComponent } from '@well-known-components/http-server' +import { Request, Response } from 'node-fetch' describe('dcl name checker: TheGraph', function () { let logs: ILoggerComponent @@ -18,7 +26,7 @@ describe('dcl name checker: TheGraph', function () { }) it('when permission asked for invalid name returns false', async () => { - const dclNameChecker = createDclNameChecker({ + const dclNameChecker = createTheGraphDclNameChecker({ logs, marketplaceSubGraph: { query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ @@ -31,7 +39,7 @@ describe('dcl name checker: TheGraph', function () { }) it('when no names returned from TheGraph returns false', async () => { - const dclNameChecker = createDclNameChecker({ + const dclNameChecker = createTheGraphDclNameChecker({ logs, marketplaceSubGraph: { query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ @@ -44,7 +52,7 @@ describe('dcl name checker: TheGraph', function () { }) it('when requested name is returned from TheGraph returns true', async () => { - const dclNameChecker = createDclNameChecker({ + const dclNameChecker = createTheGraphDclNameChecker({ logs, marketplaceSubGraph: { query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ @@ -118,3 +126,86 @@ describe('dcl name checker: on-chain', function () { await expect(dclNameChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() }) }) + +describe('name checker: endpoint', function () { + let logs: ILoggerComponent + let config: IConfigComponent + let fetch: IFetchComponent + + beforeEach(async () => { + config = createConfigComponent({ + LOG_LEVEL: 'DEBUG', + ENDPOINT_NAME_CHECKER_BASE_URL: 'http://anything' + }) + logs = await createLogComponent({ config }) + fetch = { + fetch: async (_url: Request): Promise => new Response(undefined) + } + }) + + it('when permission asked for invalid name returns false', async () => { + const permissionChecker = await createEndpointNameChecker({ + config, + fetch, + logs + }) + + await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() + }) + + it('when permission asked for invalid address returns false', async () => { + const permissionChecker = await createEndpointNameChecker({ + config, + fetch, + logs + }) + + await expect(permissionChecker.checkPermission('', 'anything')).resolves.toBeFalsy() + }) + + it.each([true, false])('when valid name and address it returns as per the endpoint', async (value) => { + fetch = { + fetch: async (_url: Request): Promise => new Response(String(value)) + } + + const permissionChecker = await createEndpointNameChecker({ + config, + fetch, + logs + }) + + const identity = await getIdentity() + const address = identity.authChain.authChain[0].payload + await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBe(value) + }) +}) + +describe('name checker: noop', function () { + let logs: ILoggerComponent + let config: IConfigComponent + let noOpNameChecker: IWorldNamePermissionChecker + + beforeEach(async () => { + config = createConfigComponent({ + LOG_LEVEL: 'DEBUG' + }) + logs = await createLogComponent({ config }) + noOpNameChecker = await createNoOpNameChecker({ + logs + }) + }) + + it('when permission asked for invalid name returns false', async () => { + await expect(noOpNameChecker.checkPermission('0xb', '')).resolves.toBeFalsy() + }) + + it('when permission asked for invalid address returns false', async () => { + await expect(noOpNameChecker.checkPermission('', 'anything')).resolves.toBeFalsy() + }) + + it('when valid name and address it returns true', async () => { + const identity = await getIdentity() + const address = identity.authChain.authChain[0].payload + await expect(noOpNameChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() + }) +}) From 804848c0e85e174e8bc61102ab83dddd5c126733 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Sun, 26 Feb 2023 16:52:32 -0500 Subject: [PATCH 02/19] refactor --- ...er.ts => world-name-permission-checker.ts} | 19 ++++++++++ src/components.ts | 36 ++++--------------- ... => world-name-permission-checker.spec.ts} | 36 +++++++++---------- 3 files changed, 44 insertions(+), 47 deletions(-) rename src/adapters/{world-name-checker.ts => world-name-permission-checker.ts} (84%) rename test/unit/{world-name-checker.spec.ts => world-name-permission-checker.spec.ts} (79%) diff --git a/src/adapters/world-name-checker.ts b/src/adapters/world-name-permission-checker.ts similarity index 84% rename from src/adapters/world-name-checker.ts rename to src/adapters/world-name-permission-checker.ts index 619559f6..0546bf9b 100644 --- a/src/adapters/world-name-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -8,6 +8,25 @@ type NamesResponse = { nfts: { name: string; owner: { id: string } }[] } +export async function createWorldNamePermissionChecker( + components: Pick +): Promise { + const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR') + switch (nameValidatorStrategy) { + case 'DCL_NAME_CHECKER': + return createTheGraphDclNameChecker(components) + case 'ON_CHAIN_DCL_NAME_CHECKER': + return await createOnChainDclNameChecker(components) + case 'ENDPOINT_NAME_CHECKER': + return await createEndpointNameChecker(components) + case 'NOOP_NAME_CHECKER': + return await createNoOpNameChecker(components) + + // Add more name validator strategies as needed here + } + throw Error(`Invalid nameValidatorStrategy selected: ${nameValidatorStrategy}`) +} + export const createTheGraphDclNameChecker = ( components: Pick ): IWorldNamePermissionChecker => { diff --git a/src/components.ts b/src/components.ts index 354b8547..5054519a 100644 --- a/src/components.ts +++ b/src/components.ts @@ -3,10 +3,12 @@ import { createServerComponent, createStatusCheckComponent } from '@well-known-c import { createLogComponent } from '@well-known-components/logger' import { createFetchComponent } from './adapters/fetch' import { createMetricsComponent } from '@well-known-components/metrics' -import { createSubgraphComponent } from '@well-known-components/thegraph-component' -import { AppComponents, GlobalContext, ICommsAdapter, IWorldNamePermissionChecker, SnsComponent } from './types' +import { + createSubgraphComponent, + metricDeclarations as theGraphMetricDeclarations +} from '@well-known-components/thegraph-component' +import { AppComponents, GlobalContext, ICommsAdapter, SnsComponent } from './types' import { metricDeclarations } from './metrics' -import { metricDeclarations as theGraphMetricDeclarations } from '@well-known-components/thegraph-component' import { HTTPProvider } from 'eth-connect' import { createAwsS3BasedFileSystemContentStorage, @@ -15,35 +17,11 @@ import { } from '@dcl/catalyst-storage' import { createStatusComponent } from './adapters/status' import { createValidator } from './adapters/validator' -import { - createTheGraphDclNameChecker, - createOnChainDclNameChecker, - createNoOpNameChecker, - createEndpointNameChecker -} from './adapters/world-name-checker' +import { createWorldNamePermissionChecker } from './adapters/world-name-permission-checker' import { createLimitsManagerComponent } from './adapters/limits-manager' import { createWorldsManagerComponent } from './adapters/worlds-manager' import { createCommsAdapterComponent } from './adapters/comms-adapter' -async function determineNameValidator( - components: Pick -) { - const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR') - switch (nameValidatorStrategy) { - case 'DCL_NAME_CHECKER': - return createTheGraphDclNameChecker(components) - case 'ON_CHAIN_DCL_NAME_CHECKER': - return await createOnChainDclNameChecker(components) - case 'ENDPOINT_NAME_CHECKER': - return await createEndpointNameChecker(components) - case 'NOOP_NAME_CHECKER': - return await createNoOpNameChecker(components) - - // Add more name validator strategies as needed here - } - throw Error(`Invalid nameValidatorStrategy selected: ${nameValidatorStrategy}`) -} - // Initialize all the components of the app export async function initComponents(): Promise { const config = await createDotEnvConfigComponent({ path: ['.env.default', '.env'] }) @@ -88,7 +66,7 @@ export async function initComponents(): Promise { arn: snsArn } - const namePermissionChecker: IWorldNamePermissionChecker = await determineNameValidator({ + const namePermissionChecker = await createWorldNamePermissionChecker({ config, ethereumProvider, fetch, diff --git a/test/unit/world-name-checker.spec.ts b/test/unit/world-name-permission-checker.spec.ts similarity index 79% rename from test/unit/world-name-checker.spec.ts rename to test/unit/world-name-permission-checker.spec.ts index 8987a5f0..f437d9ca 100644 --- a/test/unit/world-name-checker.spec.ts +++ b/test/unit/world-name-permission-checker.spec.ts @@ -5,7 +5,7 @@ import { createNoOpNameChecker, createOnChainDclNameChecker, createTheGraphDclNameChecker -} from '../../src/adapters/world-name-checker' +} from '../../src/adapters/world-name-permission-checker' import { createLogComponent } from '@well-known-components/logger' import { IConfigComponent, ILoggerComponent } from '@well-known-components/interfaces' import { getIdentity } from '../utils' @@ -26,7 +26,7 @@ describe('dcl name checker: TheGraph', function () { }) it('when permission asked for invalid name returns false', async () => { - const dclNameChecker = createTheGraphDclNameChecker({ + const permissionChecker = createTheGraphDclNameChecker({ logs, marketplaceSubGraph: { query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ @@ -35,11 +35,11 @@ describe('dcl name checker: TheGraph', function () { } }) - await expect(dclNameChecker.checkPermission('0xb', '')).resolves.toBeFalsy() + await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() }) it('when no names returned from TheGraph returns false', async () => { - const dclNameChecker = createTheGraphDclNameChecker({ + const permissionChecker = createTheGraphDclNameChecker({ logs, marketplaceSubGraph: { query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ @@ -48,11 +48,11 @@ describe('dcl name checker: TheGraph', function () { } }) - await expect(dclNameChecker.checkPermission('0xb', 'my-super-name.dcl.eth')).resolves.toBeFalsy() + await expect(permissionChecker.checkPermission('0xb', 'my-super-name.dcl.eth')).resolves.toBeFalsy() }) it('when requested name is returned from TheGraph returns true', async () => { - const dclNameChecker = createTheGraphDclNameChecker({ + const permissionChecker = createTheGraphDclNameChecker({ logs, marketplaceSubGraph: { query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ @@ -68,7 +68,7 @@ describe('dcl name checker: TheGraph', function () { } }) - await expect(dclNameChecker.checkPermission('0xb', 'my-super-name.dcl.eth')).resolves.toBeTruthy() + await expect(permissionChecker.checkPermission('0xb', 'my-super-name.dcl.eth')).resolves.toBeTruthy() }) }) @@ -85,17 +85,17 @@ describe('dcl name checker: on-chain', function () { }) it.each(['', 'name'])('when permission asked for invalid name returns false', async (name) => { - const dclNameChecker = await createOnChainDclNameChecker({ + const permissionChecker = await createOnChainDclNameChecker({ config, logs, ethereumProvider: createHttpProviderMock() }) - await expect(dclNameChecker.checkPermission('0xb', name)).resolves.toBeFalsy() + await expect(permissionChecker.checkPermission('0xb', name)).resolves.toBeFalsy() }) it('when on chain validation returns false', async () => { - const dclNameChecker = await createOnChainDclNameChecker({ + const permissionChecker = await createOnChainDclNameChecker({ config, logs, ethereumProvider: createHttpProviderMock({ @@ -107,11 +107,11 @@ describe('dcl name checker: on-chain', function () { const identity = await getIdentity() const address = identity.authChain.authChain[0].payload - await expect(dclNameChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeFalsy() + await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeFalsy() }) it('when on chain validation returns true', async () => { - const dclNameChecker = await createOnChainDclNameChecker({ + const permissionChecker = await createOnChainDclNameChecker({ config, logs, ethereumProvider: createHttpProviderMock({ @@ -123,7 +123,7 @@ describe('dcl name checker: on-chain', function () { const identity = await getIdentity() const address = identity.authChain.authChain[0].payload - await expect(dclNameChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() + await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() }) }) @@ -183,29 +183,29 @@ describe('name checker: endpoint', function () { describe('name checker: noop', function () { let logs: ILoggerComponent let config: IConfigComponent - let noOpNameChecker: IWorldNamePermissionChecker + let permissionChecker: IWorldNamePermissionChecker beforeEach(async () => { config = createConfigComponent({ LOG_LEVEL: 'DEBUG' }) logs = await createLogComponent({ config }) - noOpNameChecker = await createNoOpNameChecker({ + permissionChecker = await createNoOpNameChecker({ logs }) }) it('when permission asked for invalid name returns false', async () => { - await expect(noOpNameChecker.checkPermission('0xb', '')).resolves.toBeFalsy() + await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() }) it('when permission asked for invalid address returns false', async () => { - await expect(noOpNameChecker.checkPermission('', 'anything')).resolves.toBeFalsy() + await expect(permissionChecker.checkPermission('', 'anything')).resolves.toBeFalsy() }) it('when valid name and address it returns true', async () => { const identity = await getIdentity() const address = identity.authChain.authChain[0].payload - await expect(noOpNameChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() + await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() }) }) From 18829240aa8a754ef693d13548715052aa51d504 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Sun, 26 Feb 2023 16:56:46 -0500 Subject: [PATCH 03/19] refactor --- src/adapters/world-name-permission-checker.ts | 19 +++++++------------ .../world-name-permission-checker.spec.ts | 4 +--- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/adapters/world-name-permission-checker.ts b/src/adapters/world-name-permission-checker.ts index 0546bf9b..a9613c29 100644 --- a/src/adapters/world-name-permission-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -11,16 +11,21 @@ type NamesResponse = { export async function createWorldNamePermissionChecker( components: Pick ): Promise { + const logger = components.logs.getLogger('check-permissions') const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR') switch (nameValidatorStrategy) { case 'DCL_NAME_CHECKER': + logger.info('Using TheGraph DclNameChecker') return createTheGraphDclNameChecker(components) case 'ON_CHAIN_DCL_NAME_CHECKER': + logger.info('Using OnChain DclNameChecker') return await createOnChainDclNameChecker(components) case 'ENDPOINT_NAME_CHECKER': + logger.info('Using Endpoint NameChecker') return await createEndpointNameChecker(components) case 'NOOP_NAME_CHECKER': - return await createNoOpNameChecker(components) + logger.info('Using NoOp NameChecker') + return await createNoOpNameChecker() // Add more name validator strategies as needed here } @@ -31,7 +36,6 @@ export const createTheGraphDclNameChecker = ( components: Pick ): IWorldNamePermissionChecker => { const logger = components.logs.getLogger('check-permissions') - logger.info('Using TheGraph DclNameChecker') const cache = new LRU({ max: 100, @@ -89,7 +93,6 @@ export const createOnChainDclNameChecker = async ( components: Pick ): Promise => { const logger = components.logs.getLogger('check-permissions') - logger.info('Using OnChain DclNameChecker') const networkId = await components.config.requireString('NETWORK_ID') const networkName = networkId === '1' ? 'mainnet' : 'goerli' const factory = new ContractFactory(new RequestManager(components.ethereumProvider), checkerAbi) @@ -120,9 +123,6 @@ export const createOnChainDclNameChecker = async ( export const createEndpointNameChecker = async ( components: Pick ): Promise => { - const logger = components.logs.getLogger('check-permissions') - logger.info('Using Endpoint NameChecker') - const nameCheckUrl = await components.config.requireString('ENDPOINT_NAME_CHECKER_BASE_URL') return { @@ -141,12 +141,7 @@ export const createEndpointNameChecker = async ( } } -export const createNoOpNameChecker = async ( - components: Pick -): Promise => { - const logger = components.logs.getLogger('check-permissions') - logger.info('Using NoOp NameChecker') - +export const createNoOpNameChecker = async (): Promise => { return { checkPermission: async (ethAddress: EthAddress, worldName: string): Promise => { return !(worldName.length === 0 || ethAddress.length === 0) diff --git a/test/unit/world-name-permission-checker.spec.ts b/test/unit/world-name-permission-checker.spec.ts index f437d9ca..e64a7996 100644 --- a/test/unit/world-name-permission-checker.spec.ts +++ b/test/unit/world-name-permission-checker.spec.ts @@ -190,9 +190,7 @@ describe('name checker: noop', function () { LOG_LEVEL: 'DEBUG' }) logs = await createLogComponent({ config }) - permissionChecker = await createNoOpNameChecker({ - logs - }) + permissionChecker = await createNoOpNameChecker() }) it('when permission asked for invalid name returns false', async () => { From 8f6d5a52c49b59479f2cd58b8a45b6bc29e1a7de Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Sun, 26 Feb 2023 17:06:59 -0500 Subject: [PATCH 04/19] cr updates --- .env.default | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.default b/.env.default index 0dba6596..4297e2c4 100644 --- a/.env.default +++ b/.env.default @@ -43,9 +43,9 @@ COMMS_FIXED_ADAPTER=ws-room:ws-room-service.decentraland.org/rooms/test-scene ############################# NAME_VALIDATOR=DCL_NAME_CHECKER -#NAME_VALIDATOR=ENDPOINT_NAME_CHECKER (checks permissions against a a custom endpoint) +#NAME_VALIDATOR=ENDPOINT_NAME_CHECKER #(checks permissions against a a custom endpoint) #ENDPOINT_NAME_CHECKER_BASE_URL=https://my-custom-name-checker.com/checker -#NAME_VALIDATOR=NOOP_NAME_CHECKER (allows deployment to any valid world name by any valid ETH address) +#NAME_VALIDATOR=NOOP_NAME_CHECKER #(allows deployment to any valid world name by any valid ETH address) ########################## ## Miscelaneous configs ## From ae3206639099c72bc3a228181496d6a0ecec8197 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Sun, 26 Feb 2023 17:18:50 -0500 Subject: [PATCH 05/19] cr updates --- src/adapters/world-name-permission-checker.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/adapters/world-name-permission-checker.ts b/src/adapters/world-name-permission-checker.ts index a9613c29..07b4c7ff 100644 --- a/src/adapters/world-name-permission-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -130,13 +130,11 @@ export const createEndpointNameChecker = async ( if (worldName.length === 0 || ethAddress.length === 0) { return false } - return ( - (await components.fetch - .fetch(nameCheckUrl, { - method: 'POST' - }) - .then((response) => response.json())) === true - ) + return await components.fetch + .fetch(nameCheckUrl, { + method: 'POST' + }) + .then((response) => response.json()) } } } From 8cec6b64728314e66638d9f6bc0d5f2a1eed38d1 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Mon, 27 Feb 2023 11:57:46 -0500 Subject: [PATCH 06/19] cr updates --- .env.default | 9 +++++---- src/adapters/world-name-permission-checker.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.env.default b/.env.default index 4297e2c4..d8df5e03 100644 --- a/.env.default +++ b/.env.default @@ -11,9 +11,6 @@ HTTP_SERVER_PORT=3000 HTTP_SERVER_HOST=0.0.0.0 #HTTP_BASE_URL=https://worlds-content-server.decentraland.org -RPC_URL=https://rpc.decentraland.org/mainnet?project=worlds-content-server -MARKETPLACE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/marketplace - SNS_ARN= LAMBDAS_URL=https://peer.decentraland.org/lambdas @@ -41,7 +38,11 @@ COMMS_FIXED_ADAPTER=ws-room:ws-room-service.decentraland.org/rooms/test-scene ############################# ## Name permission checker ## ############################# -NAME_VALIDATOR=DCL_NAME_CHECKER +NAME_VALIDATOR=THE_GRAPH_DCL_NAME_CHECKER +MARKETPLACE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/marketplace + +#NAME_VALIDATOR=ON_CHAIN_DCL_NAME_CHECKER +#RPC_URL=https://rpc.decentraland.org/mainnet?project=worlds-content-server #NAME_VALIDATOR=ENDPOINT_NAME_CHECKER #(checks permissions against a a custom endpoint) #ENDPOINT_NAME_CHECKER_BASE_URL=https://my-custom-name-checker.com/checker diff --git a/src/adapters/world-name-permission-checker.ts b/src/adapters/world-name-permission-checker.ts index 07b4c7ff..af80d4c6 100644 --- a/src/adapters/world-name-permission-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -14,7 +14,7 @@ export async function createWorldNamePermissionChecker( const logger = components.logs.getLogger('check-permissions') const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR') switch (nameValidatorStrategy) { - case 'DCL_NAME_CHECKER': + case 'THE_GRAPH_DCL_NAME_CHECKER': logger.info('Using TheGraph DclNameChecker') return createTheGraphDclNameChecker(components) case 'ON_CHAIN_DCL_NAME_CHECKER': @@ -132,7 +132,11 @@ export const createEndpointNameChecker = async ( } return await components.fetch .fetch(nameCheckUrl, { - method: 'POST' + method: 'POST', + body: JSON.stringify({ + worldName: worldName, + ethAddress: ethAddress + }) }) .then((response) => response.json()) } From 1925259d8422944377ddd41159ee4c9e984ea697 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Mon, 27 Feb 2023 12:24:17 -0500 Subject: [PATCH 07/19] cr updates --- .env.default | 2 +- test/unit/world-name-permission-checker.spec.ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.env.default b/.env.default index d8df5e03..011c5c33 100644 --- a/.env.default +++ b/.env.default @@ -42,7 +42,7 @@ NAME_VALIDATOR=THE_GRAPH_DCL_NAME_CHECKER MARKETPLACE_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/decentraland/marketplace #NAME_VALIDATOR=ON_CHAIN_DCL_NAME_CHECKER -#RPC_URL=https://rpc.decentraland.org/mainnet?project=worlds-content-server +RPC_URL=https://rpc.decentraland.org/mainnet?project=worlds-content-server #NAME_VALIDATOR=ENDPOINT_NAME_CHECKER #(checks permissions against a a custom endpoint) #ENDPOINT_NAME_CHECKER_BASE_URL=https://my-custom-name-checker.com/checker diff --git a/test/unit/world-name-permission-checker.spec.ts b/test/unit/world-name-permission-checker.spec.ts index e64a7996..928bffdb 100644 --- a/test/unit/world-name-permission-checker.spec.ts +++ b/test/unit/world-name-permission-checker.spec.ts @@ -181,15 +181,9 @@ describe('name checker: endpoint', function () { }) describe('name checker: noop', function () { - let logs: ILoggerComponent - let config: IConfigComponent let permissionChecker: IWorldNamePermissionChecker beforeEach(async () => { - config = createConfigComponent({ - LOG_LEVEL: 'DEBUG' - }) - logs = await createLogComponent({ config }) permissionChecker = await createNoOpNameChecker() }) From e23af957acbee836b5ba17dad4245d19944ea78c Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Mon, 27 Feb 2023 12:30:26 -0500 Subject: [PATCH 08/19] cr updates --- src/adapters/world-name-permission-checker.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/adapters/world-name-permission-checker.ts b/src/adapters/world-name-permission-checker.ts index af80d4c6..888d82b2 100644 --- a/src/adapters/world-name-permission-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -32,9 +32,9 @@ export async function createWorldNamePermissionChecker( throw Error(`Invalid nameValidatorStrategy selected: ${nameValidatorStrategy}`) } -export const createTheGraphDclNameChecker = ( +export async function createTheGraphDclNameChecker( components: Pick -): IWorldNamePermissionChecker => { +): Promise { const logger = components.logs.getLogger('check-permissions') const cache = new LRU({ @@ -89,9 +89,9 @@ export const createTheGraphDclNameChecker = ( } } -export const createOnChainDclNameChecker = async ( +export async function createOnChainDclNameChecker( components: Pick -): Promise => { +): Promise { const logger = components.logs.getLogger('check-permissions') const networkId = await components.config.requireString('NETWORK_ID') const networkName = networkId === '1' ? 'mainnet' : 'goerli' @@ -120,9 +120,9 @@ export const createOnChainDclNameChecker = async ( } } -export const createEndpointNameChecker = async ( +export async function createEndpointNameChecker( components: Pick -): Promise => { +): Promise { const nameCheckUrl = await components.config.requireString('ENDPOINT_NAME_CHECKER_BASE_URL') return { @@ -130,20 +130,21 @@ export const createEndpointNameChecker = async ( if (worldName.length === 0 || ethAddress.length === 0) { return false } - return await components.fetch - .fetch(nameCheckUrl, { - method: 'POST', - body: JSON.stringify({ - worldName: worldName, - ethAddress: ethAddress - }) + + const res = await components.fetch.fetch(nameCheckUrl, { + method: 'POST', + body: JSON.stringify({ + worldName: worldName, + ethAddress: ethAddress }) - .then((response) => response.json()) + }) + + return res.json() } } } -export const createNoOpNameChecker = async (): Promise => { +export async function createNoOpNameChecker(): Promise { return { checkPermission: async (ethAddress: EthAddress, worldName: string): Promise => { return !(worldName.length === 0 || ethAddress.length === 0) From ed82b769e716ed7a4049d1db9ea79685daf454c8 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Mon, 27 Feb 2023 12:32:17 -0500 Subject: [PATCH 09/19] cr updates --- test/unit/world-name-permission-checker.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/world-name-permission-checker.spec.ts b/test/unit/world-name-permission-checker.spec.ts index 928bffdb..4978a564 100644 --- a/test/unit/world-name-permission-checker.spec.ts +++ b/test/unit/world-name-permission-checker.spec.ts @@ -26,7 +26,7 @@ describe('dcl name checker: TheGraph', function () { }) it('when permission asked for invalid name returns false', async () => { - const permissionChecker = createTheGraphDclNameChecker({ + const permissionChecker = await createTheGraphDclNameChecker({ logs, marketplaceSubGraph: { query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ @@ -39,7 +39,7 @@ describe('dcl name checker: TheGraph', function () { }) it('when no names returned from TheGraph returns false', async () => { - const permissionChecker = createTheGraphDclNameChecker({ + const permissionChecker = await createTheGraphDclNameChecker({ logs, marketplaceSubGraph: { query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ @@ -52,7 +52,7 @@ describe('dcl name checker: TheGraph', function () { }) it('when requested name is returned from TheGraph returns true', async () => { - const permissionChecker = createTheGraphDclNameChecker({ + const permissionChecker = await createTheGraphDclNameChecker({ logs, marketplaceSubGraph: { query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ From d9602385b9ebc1008a77eb3a010bb85a10b2f761 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Tue, 28 Feb 2023 11:24:09 -0500 Subject: [PATCH 10/19] split mandatory and optional validations --- src/adapters/validator.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/adapters/validator.ts b/src/adapters/validator.ts index d727f10d..a1358040 100644 --- a/src/adapters/validator.ts +++ b/src/adapters/validator.ts @@ -259,21 +259,24 @@ export const validateSdkVersion: Validation = async ( return OK } -const quickValidations: Validation[] = [ - validateEntityId, +const mandatoryValidations: Validation[] = [ validateEntity, + validateEntityId, + validateDeploymentTtl, validateAuthChain, validateSigner, validateSignature, - validateDeploymentTtl, - validateSceneDimensions, + validateDeploymentPermission, // Mirar este mejor validateFiles - // validateSdkVersion TODO re-enable (and test) once SDK7 is ready ] -const slowValidations: Validation[] = [validateSize, validateDeploymentPermission] +const optionalValidations: Validation[] = [ + validateSceneDimensions, + validateSize + // validateSdkVersion TODO re-enable (and test) once SDK7 is ready +] -const allValidations: Validation[] = [...quickValidations, ...slowValidations] +const allValidations: Validation[] = [...mandatoryValidations, ...optionalValidations] export const createValidator = (components: ValidatorComponents): Validator => ({ async validate(deployment: DeploymentToValidate): Promise { From b0e44954dd91d60d5824d12e51265a310cb7b0eb Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Tue, 28 Feb 2023 11:46:11 -0500 Subject: [PATCH 11/19] new validation api skeleton --- src/adapters/world-name-permission-checker.ts | 45 +++++++++++++++---- src/types.ts | 5 +++ test/components.ts | 2 +- ... => world-name-permission-checker-mock.ts} | 8 +++- test/unit/validator.spec.ts | 2 +- 5 files changed, 49 insertions(+), 13 deletions(-) rename test/mocks/{dcl-name-checker-mock.ts => world-name-permission-checker-mock.ts} (65%) diff --git a/src/adapters/world-name-permission-checker.ts b/src/adapters/world-name-permission-checker.ts index 888d82b2..845e26cc 100644 --- a/src/adapters/world-name-permission-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -1,4 +1,4 @@ -import { AppComponents, IWorldNamePermissionChecker } from '../types' +import { AppComponents, DeploymentToValidate, IWorldNameOwnershipChecker, IWorldNamePermissionChecker } from '../types' import { EthAddress } from '@dcl/schemas' import LRU from 'lru-cache' import { ContractFactory, RequestManager } from 'eth-connect' @@ -34,7 +34,7 @@ export async function createWorldNamePermissionChecker( export async function createTheGraphDclNameChecker( components: Pick -): Promise { +): Promise { const logger = components.logs.getLogger('check-permissions') const cache = new LRU({ @@ -75,7 +75,7 @@ export async function createTheGraphDclNameChecker( } }) - const checkPermission = async (ethAddress: EthAddress, worldName: string): Promise => { + async function checkOwnership(ethAddress: EthAddress, worldName: string): Promise { if (worldName.length === 0) { return false } @@ -85,20 +85,30 @@ export async function createTheGraphDclNameChecker( } return { - checkPermission + checkOwnership, + checkPermission: async function (ethAddress: EthAddress, worldName: string): Promise { + if (await checkOwnership(ethAddress, worldName)) { + return true + } + // TODO check ACL + return false + }, + validate(deployment: DeploymentToValidate): Promise { + return Promise.resolve(false) + } } } export async function createOnChainDclNameChecker( components: Pick -): Promise { +): Promise { const logger = components.logs.getLogger('check-permissions') const networkId = await components.config.requireString('NETWORK_ID') const networkName = networkId === '1' ? 'mainnet' : 'goerli' const factory = new ContractFactory(new RequestManager(components.ethereumProvider), checkerAbi) const checker = (await factory.at(checkerContracts[networkName])) as any - const checkPermission = async (ethAddress: EthAddress, worldName: string): Promise => { + async function checkOwnership(ethAddress: EthAddress, worldName: string): Promise { if (worldName.length === 0 || !worldName.endsWith('.dcl.eth')) { return false } @@ -116,7 +126,17 @@ export async function createOnChainDclNameChecker( } return { - checkPermission + checkOwnership, + checkPermission: async function (ethAddress: EthAddress, worldName: string): Promise { + if (await checkOwnership(ethAddress, worldName)) { + return true + } + // TODO check ACL + return false + }, + validate(deployment: DeploymentToValidate): Promise { + return Promise.resolve(false) + } } } @@ -140,14 +160,21 @@ export async function createEndpointNameChecker( }) return res.json() + }, + validate(deployment: DeploymentToValidate): Promise { + return Promise.resolve(false) } } } export async function createNoOpNameChecker(): Promise { + async function checkPermission(ethAddress: EthAddress, worldName: string): Promise { + return !(worldName.length === 0 || ethAddress.length === 0) + } return { - checkPermission: async (ethAddress: EthAddress, worldName: string): Promise => { - return !(worldName.length === 0 || ethAddress.length === 0) + checkPermission, + validate(deployment: DeploymentToValidate): Promise { + return Promise.resolve(true) } } } diff --git a/src/types.ts b/src/types.ts index d14da3ee..af5c8334 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,6 +55,11 @@ export type Validation = ( export type IWorldNamePermissionChecker = { checkPermission(ethAddress: EthAddress, worldName: string): Promise + validate(deployment: DeploymentToValidate): Promise +} + +export type IWorldNameOwnershipChecker = { + checkOwnership(ethAddress: EthAddress, worldName: string): Promise } export type ContentStatus = { diff --git a/test/components.ts b/test/components.ts index ad0c5e4d..3aa99a09 100644 --- a/test/components.ts +++ b/test/components.ts @@ -7,7 +7,7 @@ import { main } from '../src/service' import { TestComponents } from '../src/types' import { initComponents as originalInitComponents } from '../src/components' import { createMockMarketplaceSubGraph } from './mocks/marketplace-subgraph-mock' -import { createMockNamePermissionChecker } from './mocks/dcl-name-checker-mock' +import { createMockNamePermissionChecker } from './mocks/world-name-permission-checker-mock' import { createValidator } from '../src/adapters/validator' import { createFetchComponent } from '../src/adapters/fetch' import { createMockLimitsManagerComponent } from './mocks/limits-manager-mock' diff --git a/test/mocks/dcl-name-checker-mock.ts b/test/mocks/world-name-permission-checker-mock.ts similarity index 65% rename from test/mocks/dcl-name-checker-mock.ts rename to test/mocks/world-name-permission-checker-mock.ts index 058c3587..e9bc9a51 100644 --- a/test/mocks/dcl-name-checker-mock.ts +++ b/test/mocks/world-name-permission-checker-mock.ts @@ -1,5 +1,5 @@ import { EthAddress } from '@dcl/schemas' -import { IWorldNamePermissionChecker } from '../../src/types' +import { DeploymentToValidate, IWorldNamePermissionChecker } from '../../src/types' export function createMockNamePermissionChecker(names?: string[]): IWorldNamePermissionChecker { const checkPermission = async (_ethAddress: EthAddress, worldName: string): Promise => { @@ -9,7 +9,11 @@ export function createMockNamePermissionChecker(names?: string[]): IWorldNamePer return names && names.map((name) => name.toLowerCase()).includes(worldName.toLowerCase()) } + return { - checkPermission + checkPermission, + validate(deployment: DeploymentToValidate): Promise { + return Promise.resolve(false) + } } } diff --git a/test/unit/validator.spec.ts b/test/unit/validator.spec.ts index 43edf657..97ca2131 100644 --- a/test/unit/validator.spec.ts +++ b/test/unit/validator.spec.ts @@ -24,7 +24,7 @@ import { import { HTTPProvider, stringToUtf8Bytes } from 'eth-connect' import { EntityType } from '@dcl/schemas' import { createMockLimitsManagerComponent } from '../mocks/limits-manager-mock' -import { createMockNamePermissionChecker } from '../mocks/dcl-name-checker-mock' +import { createMockNamePermissionChecker } from '../mocks/world-name-permission-checker-mock' import { DeploymentBuilder } from 'dcl-catalyst-client' import { getIdentity } from '../utils' import { Authenticator, AuthIdentity } from '@dcl/crypto' From 533ba630779a82454b852e33926a69f740a0d6e0 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Tue, 28 Feb 2023 16:33:22 -0500 Subject: [PATCH 12/19] big refactor --- src/adapters/dcl-name-checker.ts | 131 ++++++++++ src/adapters/validator.ts | 6 +- src/adapters/world-name-permission-checker.ts | 124 +-------- src/components.ts | 29 +-- src/controllers/handlers/acl-handlers.ts | 16 +- .../handlers/deploy-entity-handler.ts | 2 +- src/types.ts | 7 +- test/components.ts | 2 - test/integration/acl-handlers.spec.ts | 52 ++-- test/unit/dcl-name-checker.spec.ts | 245 ++++++++++++++++++ test/unit/validator.spec.ts | 3 - .../world-name-permission-checker.spec.ts | 108 ++------ 12 files changed, 447 insertions(+), 278 deletions(-) create mode 100644 src/adapters/dcl-name-checker.ts create mode 100644 test/unit/dcl-name-checker.spec.ts diff --git a/src/adapters/dcl-name-checker.ts b/src/adapters/dcl-name-checker.ts new file mode 100644 index 00000000..c9bfb25b --- /dev/null +++ b/src/adapters/dcl-name-checker.ts @@ -0,0 +1,131 @@ +import { AppComponents, IDclNameChecker } from '../types' +import { EthAddress } from '@dcl/schemas' +import LRU from 'lru-cache' +import { ContractFactory, HTTPProvider, RequestManager } from 'eth-connect' +import { checkerAbi, checkerContracts, registrarContracts } from '@dcl/catalyst-contracts' +import { createSubgraphComponent } from '@well-known-components/thegraph-component' + +type NamesResponse = { + nfts: { name: string; owner: { id: string } }[] +} + +export async function createDclNameChecker( + components: Pick +): Promise { + const logger = components.logs.getLogger('dcl-name checker') + const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR') + switch (nameValidatorStrategy) { + case 'THE_GRAPH_DCL_NAME_CHECKER': + logger.info('Using TheGraph DclNameChecker') + return createTheGraphDclNameChecker(components) + case 'ON_CHAIN_DCL_NAME_CHECKER': + logger.info('Using OnChain DclNameChecker') + return await createOnChainDclNameChecker(components) + } + logger.info('No support for DCL name checking') + return await createNoOwnershipSupportedNameChecker() +} + +export async function createTheGraphDclNameChecker( + components: Pick +): Promise { + const logger = components.logs.getLogger('dcl-name checker') + + const subGraphUrl = await components.config.requireString('MARKETPLACE_SUBGRAPH_URL') + const marketplaceSubGraph = await createSubgraphComponent(components, subGraphUrl) + + const cache = new LRU({ + max: 100, + ttl: 5 * 60 * 1000, // cache for 5 minutes + fetchMethod: async (worldName: string): Promise => { + /* + DCL owners are case-sensitive, so when searching by dcl name in TheGraph we + need to do a case-insensitive search because the worldName provided as fetch key + may not be in the exact same case of the registered name. There are several methods + suffixed _nocase, but not one for equality, so this is a bit hackish, but it works. + */ + const result = await marketplaceSubGraph.query( + ` + query FetchOwnerForDclName($worldName: String) { + nfts( + where: {name_starts_with_nocase: $worldName, name_ends_with_nocase: $worldName, category: ens} + orderBy: name + first: 1000 + ) { + name + owner { + id + } + } + }`, + { + worldName: worldName.toLowerCase().replace('.dcl.eth', '') + } + ) + logger.info(`Fetched owner of world ${worldName}: ${result.nfts.map(({ owner }) => owner.id.toLowerCase())}`) + + const owners = result.nfts + .filter((nft) => `${nft.name.toLowerCase()}.dcl.eth` === worldName.toLowerCase()) + .map(({ owner }) => owner.id.toLowerCase()) + + return owners.length > 0 ? owners[0] : undefined + } + }) + + async function checkOwnership(ethAddress: EthAddress, worldName: string): Promise { + if (worldName.length === 0) { + return false + } + + const owner = await cache.fetch(worldName) + return !!owner && owner === ethAddress.toLowerCase() + } + + return { + checkOwnership + } +} + +export async function createOnChainDclNameChecker( + components: Pick +): Promise { + const logger = components.logs.getLogger('dcl-name checker') + + const rpcUrl = await components.config.requireString('RPC_URL') + const ethereumProvider = new HTTPProvider(rpcUrl, components.fetch) + const networkId = await components.config.requireString('NETWORK_ID') + const networkName = networkId === '1' ? 'mainnet' : 'goerli' + const factory = new ContractFactory(new RequestManager(ethereumProvider), checkerAbi) + const checker = (await factory.at(checkerContracts[networkName])) as any + + async function checkOwnership(ethAddress: EthAddress, worldName: string): Promise { + if (worldName.length === 0 || !worldName.endsWith('.dcl.eth')) { + return false + } + + const hasPermission = await checker.checkName( + ethAddress, + registrarContracts[networkName], + worldName.replace('.dcl.eth', ''), + 'latest' + ) + + logger.debug(`Checking name ${worldName} for address ${ethAddress}: ${hasPermission}`) + + return hasPermission + } + + return { + checkOwnership + } +} + +export async function createNoOwnershipSupportedNameChecker(): Promise { + async function checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { + return false + } + + return { + checkOwnership + } +} diff --git a/src/adapters/validator.ts b/src/adapters/validator.ts index a1358040..9544b540 100644 --- a/src/adapters/validator.ts +++ b/src/adapters/validator.ts @@ -97,7 +97,7 @@ export const validateSignature: Validation = async ( const result = await Authenticator.validateSignature( deployment.entity.id, deployment.authChain, - components.ethereumProvider, + undefined, // No need because it's not EIP1654 validator 10 ) @@ -266,8 +266,8 @@ const mandatoryValidations: Validation[] = [ validateAuthChain, validateSigner, validateSignature, - validateDeploymentPermission, // Mirar este mejor - validateFiles + validateFiles, + validateDeploymentPermission ] const optionalValidations: Validation[] = [ diff --git a/src/adapters/world-name-permission-checker.ts b/src/adapters/world-name-permission-checker.ts index 845e26cc..ec2916d3 100644 --- a/src/adapters/world-name-permission-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -1,140 +1,38 @@ -import { AppComponents, DeploymentToValidate, IWorldNameOwnershipChecker, IWorldNamePermissionChecker } from '../types' +import { AppComponents, DeploymentToValidate, IWorldNamePermissionChecker } from '../types' import { EthAddress } from '@dcl/schemas' -import LRU from 'lru-cache' -import { ContractFactory, RequestManager } from 'eth-connect' -import { checkerAbi, checkerContracts, registrarContracts } from '@dcl/catalyst-contracts' - -type NamesResponse = { - nfts: { name: string; owner: { id: string } }[] -} export async function createWorldNamePermissionChecker( - components: Pick + components: Pick ): Promise { const logger = components.logs.getLogger('check-permissions') const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR') switch (nameValidatorStrategy) { case 'THE_GRAPH_DCL_NAME_CHECKER': - logger.info('Using TheGraph DclNameChecker') - return createTheGraphDclNameChecker(components) case 'ON_CHAIN_DCL_NAME_CHECKER': - logger.info('Using OnChain DclNameChecker') - return await createOnChainDclNameChecker(components) + logger.info('Using DclNameChecker + ACL') + return createDclNamePlusACLPermissionChecker(components) case 'ENDPOINT_NAME_CHECKER': logger.info('Using Endpoint NameChecker') return await createEndpointNameChecker(components) case 'NOOP_NAME_CHECKER': logger.info('Using NoOp NameChecker') return await createNoOpNameChecker() - - // Add more name validator strategies as needed here } throw Error(`Invalid nameValidatorStrategy selected: ${nameValidatorStrategy}`) } -export async function createTheGraphDclNameChecker( - components: Pick -): Promise { - const logger = components.logs.getLogger('check-permissions') - - const cache = new LRU({ - max: 100, - ttl: 5 * 60 * 1000, // cache for 5 minutes - fetchMethod: async (worldName: string): Promise => { - /* - DCL owners are case-sensitive, so when searching by dcl name in TheGraph we - need to do a case-insensitive search because the worldName provided as fetch key - may not be in the exact same case of the registered name. There are several methods - suffixed _nocase, but not one for equality, so this is a bit hackish, but it works. - */ - const result = await components.marketplaceSubGraph.query( - ` - query FetchOwnerForDclName($worldName: String) { - nfts( - where: {name_starts_with_nocase: $worldName, name_ends_with_nocase: $worldName, category: ens} - orderBy: name - first: 1000 - ) { - name - owner { - id - } - } - }`, - { - worldName: worldName.toLowerCase().replace('.dcl.eth', '') - } - ) - logger.info(`Fetched owner of world ${worldName}: ${result.nfts.map(({ owner }) => owner.id.toLowerCase())}`) - - const owners = result.nfts - .filter((nft) => `${nft.name.toLowerCase()}.dcl.eth` === worldName.toLowerCase()) - .map(({ owner }) => owner.id.toLowerCase()) - - return owners.length > 0 ? owners[0] : undefined - } - }) - - async function checkOwnership(ethAddress: EthAddress, worldName: string): Promise { - if (worldName.length === 0) { - return false - } - - const owner = await cache.fetch(worldName) - return !!owner && owner === ethAddress.toLowerCase() - } - - return { - checkOwnership, - checkPermission: async function (ethAddress: EthAddress, worldName: string): Promise { - if (await checkOwnership(ethAddress, worldName)) { - return true - } - // TODO check ACL - return false - }, - validate(deployment: DeploymentToValidate): Promise { - return Promise.resolve(false) - } - } -} - -export async function createOnChainDclNameChecker( - components: Pick -): Promise { - const logger = components.logs.getLogger('check-permissions') - const networkId = await components.config.requireString('NETWORK_ID') - const networkName = networkId === '1' ? 'mainnet' : 'goerli' - const factory = new ContractFactory(new RequestManager(components.ethereumProvider), checkerAbi) - const checker = (await factory.at(checkerContracts[networkName])) as any - - async function checkOwnership(ethAddress: EthAddress, worldName: string): Promise { - if (worldName.length === 0 || !worldName.endsWith('.dcl.eth')) { - return false - } - - const hasPermission = await checker.checkName( - ethAddress, - registrarContracts[networkName], - worldName.replace('.dcl.eth', ''), - 'latest' - ) - - logger.debug(`Checking name ${worldName} for address ${ethAddress}: ${hasPermission}`) - - return hasPermission - } - +export async function createDclNamePlusACLPermissionChecker( + components: Pick +): Promise { return { - checkOwnership, checkPermission: async function (ethAddress: EthAddress, worldName: string): Promise { - if (await checkOwnership(ethAddress, worldName)) { + if (await components.dclNameChecker.checkOwnership(ethAddress, worldName)) { return true } // TODO check ACL return false }, - validate(deployment: DeploymentToValidate): Promise { + validate(_deployment: DeploymentToValidate): Promise { return Promise.resolve(false) } } @@ -161,7 +59,7 @@ export async function createEndpointNameChecker( return res.json() }, - validate(deployment: DeploymentToValidate): Promise { + validate(_deployment: DeploymentToValidate): Promise { return Promise.resolve(false) } } @@ -173,7 +71,7 @@ export async function createNoOpNameChecker(): Promise { + validate(_deployment: DeploymentToValidate): Promise { return Promise.resolve(true) } } diff --git a/src/components.ts b/src/components.ts index 5054519a..f7c6e7b3 100644 --- a/src/components.ts +++ b/src/components.ts @@ -3,13 +3,9 @@ import { createServerComponent, createStatusCheckComponent } from '@well-known-c import { createLogComponent } from '@well-known-components/logger' import { createFetchComponent } from './adapters/fetch' import { createMetricsComponent } from '@well-known-components/metrics' -import { - createSubgraphComponent, - metricDeclarations as theGraphMetricDeclarations -} from '@well-known-components/thegraph-component' +import { metricDeclarations as theGraphMetricDeclarations } from '@well-known-components/thegraph-component' import { AppComponents, GlobalContext, ICommsAdapter, SnsComponent } from './types' import { metricDeclarations } from './metrics' -import { HTTPProvider } from 'eth-connect' import { createAwsS3BasedFileSystemContentStorage, createFolderBasedFileSystemContentStorage, @@ -21,6 +17,7 @@ import { createWorldNamePermissionChecker } from './adapters/world-name-permissi import { createLimitsManagerComponent } from './adapters/limits-manager' import { createWorldsManagerComponent } from './adapters/worlds-manager' import { createCommsAdapterComponent } from './adapters/comms-adapter' +import { createDclNameChecker } from './adapters/dcl-name-checker' // Initialize all the components of the app export async function initComponents(): Promise { @@ -43,9 +40,6 @@ export async function initComponents(): Promise { const commsAdapter: ICommsAdapter = await createCommsAdapterComponent({ config, fetch, logs }) - const rpcUrl = await config.requireString('RPC_URL') - const ethereumProvider = new HTTPProvider(rpcUrl, fetch) - const storageFolder = (await config.getString('STORAGE_FOLDER')) || 'contents' const bucket = await config.getString('BUCKET') @@ -55,9 +49,6 @@ export async function initComponents(): Promise { ? await createAwsS3BasedFileSystemContentStorage({ fs, config }, bucket) : await createFolderBasedFileSystemContentStorage({ fs }, storageFolder) - const subGraphUrl = await config.requireString('MARKETPLACE_SUBGRAPH_URL') - const marketplaceSubGraph = await createSubgraphComponent({ config, logs, metrics, fetch }, subGraphUrl) - const snsArn = await config.getString('SNS_ARN') const status = await createStatusComponent({ logs, fetch, config }) @@ -66,12 +57,18 @@ export async function initComponents(): Promise { arn: snsArn } - const namePermissionChecker = await createWorldNamePermissionChecker({ + const dclNameChecker = await createDclNameChecker({ config, - ethereumProvider, fetch, logs, - marketplaceSubGraph + metrics + }) + + const namePermissionChecker = await createWorldNamePermissionChecker({ + config, + dclNameChecker, + fetch, + logs }) const limitsManager = await createLimitsManagerComponent({ config, fetch, logs }) @@ -81,13 +78,13 @@ export async function initComponents(): Promise { const validator = createValidator({ config, namePermissionChecker, - ethereumProvider, limitsManager, storage, worldsManager }) return { + dclNameChecker, commsAdapter, config, namePermissionChecker, @@ -96,9 +93,7 @@ export async function initComponents(): Promise { statusChecks, fetch, metrics, - ethereumProvider, storage, - marketplaceSubGraph, limitsManager, sns, status, diff --git a/src/controllers/handlers/acl-handlers.ts b/src/controllers/handlers/acl-handlers.ts index b55857bc..10474de9 100644 --- a/src/controllers/handlers/acl-handlers.ts +++ b/src/controllers/handlers/acl-handlers.ts @@ -3,9 +3,9 @@ import { AccessControlList, HandlerContextWithPath } from '../../types' import { AuthChain, EthAddress } from '@dcl/schemas' export async function getAclHandler( - ctx: HandlerContextWithPath<'namePermissionChecker' | 'worldsManager', '/acl/:world_name'> + ctx: HandlerContextWithPath<'dclNameChecker' | 'worldsManager', '/acl/:world_name'> ): Promise { - const { namePermissionChecker, worldsManager } = ctx.components + const { dclNameChecker, worldsManager } = ctx.components const worldName = ctx.params.world_name @@ -22,8 +22,8 @@ export async function getAclHandler( // Check that the ACL was signed by the wallet that currently owns the world, or else return empty const ethAddress = worldMetadata.acl[0].payload - const permission = await namePermissionChecker.checkPermission(ethAddress, worldName) - const acl: AccessControlList = !permission + const isOwner = await dclNameChecker.checkOwnership(ethAddress, worldName) + const acl: AccessControlList = !isOwner ? { resource: worldName, allowed: [] @@ -38,9 +38,9 @@ export async function getAclHandler( } export async function postAclHandler( - ctx: HandlerContextWithPath<'namePermissionChecker' | 'worldsManager', '/acl/:world_name'> + ctx: HandlerContextWithPath<'dclNameChecker' | 'worldsManager', '/acl/:world_name'> ): Promise { - const { namePermissionChecker, worldsManager } = ctx.components + const { dclNameChecker, worldsManager } = ctx.components const worldName = ctx.params.world_name @@ -54,8 +54,8 @@ export async function postAclHandler( } } - const permission = await namePermissionChecker.checkPermission(authChain[0].payload, worldName) - if (!permission) { + const isOwner = await dclNameChecker.checkOwnership(authChain[0].payload, worldName) + if (!isOwner) { return { status: 403, body: { diff --git a/src/controllers/handlers/deploy-entity-handler.ts b/src/controllers/handlers/deploy-entity-handler.ts index 9a3b61b7..9467e120 100644 --- a/src/controllers/handlers/deploy-entity-handler.ts +++ b/src/controllers/handlers/deploy-entity-handler.ts @@ -79,7 +79,7 @@ async function storeEntity( export async function deployEntity( ctx: FormDataContext & HandlerContextWithPath< - 'config' | 'ethereumProvider' | 'logs' | 'namePermissionChecker' | 'metrics' | 'storage' | 'sns' | 'validator', + 'config' | 'logs' | 'namePermissionChecker' | 'metrics' | 'storage' | 'sns' | 'validator', '/entities' > ): Promise { diff --git a/src/types.ts b/src/types.ts index af5c8334..3841d06d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,7 +45,7 @@ export type ValidationResult = { export type ValidatorComponents = Pick< AppComponents, - 'config' | 'namePermissionChecker' | 'ethereumProvider' | 'limitsManager' | 'storage' | 'worldsManager' + 'config' | 'namePermissionChecker' | 'limitsManager' | 'storage' | 'worldsManager' > export type Validation = ( @@ -58,7 +58,7 @@ export type IWorldNamePermissionChecker = { validate(deployment: DeploymentToValidate): Promise } -export type IWorldNameOwnershipChecker = { +export type IDclNameChecker = { checkOwnership(ethAddress: EthAddress, worldName: string): Promise } @@ -106,6 +106,7 @@ export type IWorldsManager = { // components used in every environment export type BaseComponents = { + dclNameChecker: IDclNameChecker commsAdapter: ICommsAdapter config: IConfigComponent namePermissionChecker: IWorldNamePermissionChecker @@ -113,9 +114,7 @@ export type BaseComponents = { server: IHttpServerComponent fetch: IFetchComponent metrics: IMetricsComponent - ethereumProvider: HTTPProvider storage: IContentStorageComponent - marketplaceSubGraph: ISubgraphComponent limitsManager: ILimitsManager status: IStatusComponent sns: SnsComponent diff --git a/test/components.ts b/test/components.ts index 3aa99a09..28a07ea2 100644 --- a/test/components.ts +++ b/test/components.ts @@ -50,7 +50,6 @@ async function initComponents(): Promise { storage, namePermissionChecker, limitsManager, - ethereumProvider: components.ethereumProvider, worldsManager }) const status = createMockStatusComponent() @@ -58,7 +57,6 @@ async function initComponents(): Promise { return { ...components, localFetch: await createLocalFetchCompoment(config), - marketplaceSubGraph: createMockMarketplaceSubGraph(), namePermissionChecker: namePermissionChecker, commsAdapter, fetch, diff --git a/test/integration/acl-handlers.spec.ts b/test/integration/acl-handlers.spec.ts index f661a2ed..4c3873f2 100644 --- a/test/integration/acl-handlers.spec.ts +++ b/test/integration/acl-handlers.spec.ts @@ -64,7 +64,7 @@ test('acl handler GET /acl/:world_name', function ({ components }) { test('acl handler GET /acl/:world_name', function ({ components, stubComponents }) { it('returns acl from auth-chain when acl exists', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const delegatedIdentity = await getIdentity() const ownerIdentity = await getIdentity() @@ -76,7 +76,7 @@ test('acl handler GET /acl/:world_name', function ({ components, stubComponents acl: Authenticator.signPayload(ownerIdentity.authChain, payload) }) - namePermissionChecker.checkPermission + dclNameChecker.checkOwnership .withArgs(ownerIdentity.authChain.authChain[0].payload, 'my-world.dcl.eth') .resolves(true) @@ -93,7 +93,7 @@ test('acl handler GET /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('works when all is correct', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() const delegatedIdentity = await getIdentity() @@ -102,9 +102,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents entityId: 'bafkreiax5plaxze77tnjbnozga7dsbefdh53horza4adf2xjzxo3k5i4xq' }) - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const payload = `{"resource":"my-world.dcl.eth","allowed":["${delegatedIdentity.realAccount.address}"]}` @@ -133,7 +131,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when resource is different than requested world', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() const delegatedIdentity = await getIdentity() @@ -144,9 +142,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents entityId: 'bafkreiax5plaxze77tnjbnozga7dsbefdh53horza4adf2xjzxo3k5i4xq' }) - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const acl = Authenticator.signPayload(identity.authChain, payload) @@ -166,7 +162,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when name owner is part of the ACL', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() @@ -176,9 +172,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents entityId: 'bafkreiax5plaxze77tnjbnozga7dsbefdh53horza4adf2xjzxo3k5i4xq' }) - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const acl = Authenticator.signPayload(identity.authChain, payload) @@ -198,7 +192,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when invalid acl (acl is not array)', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() const delegatedIdentity = await getIdentity() @@ -209,9 +203,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents entityId: 'bafkreiax5plaxze77tnjbnozga7dsbefdh53horza4adf2xjzxo3k5i4xq' }) - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const acl = Authenticator.signPayload(identity.authChain, payload) @@ -230,7 +222,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when invalid acl (non address)', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() @@ -240,9 +232,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents entityId: 'bafkreiax5plaxze77tnjbnozga7dsbefdh53horza4adf2xjzxo3k5i4xq' }) - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const acl = Authenticator.signPayload(identity.authChain, payload) @@ -261,7 +251,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when signer wallet does not own world name', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() const delegatedIdentity = await getIdentity() @@ -272,9 +262,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents entityId: 'bafkreiax5plaxze77tnjbnozga7dsbefdh53horza4adf2xjzxo3k5i4xq' }) - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(false) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(false) const acl = Authenticator.signPayload(identity.authChain, payload) @@ -293,7 +281,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails invalid payload sent', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() @@ -301,9 +289,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents entityId: 'bafkreiax5plaxze77tnjbnozga7dsbefdh53horza4adf2xjzxo3k5i4xq' }) - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(false) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(false) const r = await localFetch.fetch('/acl/my-world.dcl.eth', { body: JSON.stringify({}), @@ -320,14 +306,12 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when the world name does not exist', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() const delegatedIdentity = await getIdentity() - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const payload = `{"resource":"my-world.dcl.eth","allowed":["${delegatedIdentity.realAccount.address}"]}` diff --git a/test/unit/dcl-name-checker.spec.ts b/test/unit/dcl-name-checker.spec.ts new file mode 100644 index 00000000..6308634d --- /dev/null +++ b/test/unit/dcl-name-checker.spec.ts @@ -0,0 +1,245 @@ +import { createConfigComponent } from '@well-known-components/env-config-provider' +import { + metricDeclarations, + metricDeclarations as theGraphMetricDeclarations +} from '@well-known-components/thegraph-component' +import { createLogComponent } from '@well-known-components/logger' +import { IConfigComponent, ILoggerComponent, IMetricsComponent } from '@well-known-components/interfaces' +import { getIdentity } from '../utils' +import { IFetchComponent } from '@well-known-components/http-server' +import { Request, Response } from 'node-fetch' +import { + createDclNameChecker, + createNoOwnershipSupportedNameChecker, + createOnChainDclNameChecker, + createTheGraphDclNameChecker +} from '../../src/adapters/dcl-name-checker' +import { createTestMetricsComponent } from '@well-known-components/metrics' +import { IDclNameChecker } from '../../src/types' + +describe('strategy builder', function () { + let config: IConfigComponent + let fetch: IFetchComponent + let logs: ILoggerComponent + let metrics: IMetricsComponent + + beforeEach(async () => { + fetch = { + fetch: async (_url: Request): Promise => + new Response( + JSON.stringify({ + data: { nfts: [] } + }) + ) + } + + metrics = createTestMetricsComponent(theGraphMetricDeclarations) + }) + + it('it can build a TheGraph DclNameChecker', async () => { + await expect( + createDclNameChecker({ + config: createConfigComponent({ + LOG_LEVEL: 'DEBUG', + NAME_VALIDATOR: 'THE_GRAPH_DCL_NAME_CHECKER', + MARKETPLACE_SUBGRAPH_URL: 'https://subgraph.com' + }), + fetch, + logs: await createLogComponent({ + config + }), + metrics + }) + ).resolves.toBeDefined() + }) + + it('it can build an OnChain DclNameChecker', async () => { + await expect( + createDclNameChecker({ + config: createConfigComponent({ + LOG_LEVEL: 'DEBUG', + NAME_VALIDATOR: 'ON_CHAIN_DCL_NAME_CHECKER', + RPC_URL: 'https://rpc.com', + NETWORK_ID: '1' + }), + fetch, + logs: await createLogComponent({ + config + }), + metrics + }) + ).resolves.toBeDefined() + }) + + it('it can build an OnChain DclNameChecker', async () => { + await expect( + createDclNameChecker({ + config: createConfigComponent({ + LOG_LEVEL: 'DEBUG', + NAME_VALIDATOR: 'OTHER' + }), + fetch, + logs: await createLogComponent({ + config + }), + metrics + }) + ).resolves.toBeDefined() + }) +}) + +describe('dcl name checker: TheGraph', function () { + let config: IConfigComponent + let logs: ILoggerComponent + let metrics: IMetricsComponent + + beforeEach(async () => { + config = createConfigComponent({ + LOG_LEVEL: 'DEBUG', + MARKETPLACE_SUBGRAPH_URL: '' + }) + logs = await createLogComponent({ + config + }) + metrics = createTestMetricsComponent(theGraphMetricDeclarations) + }) + + it('when permission asked for invalid name returns false', async () => { + const dclNameChecker = await createTheGraphDclNameChecker({ + config, + logs, + fetch: { + fetch: async (_url: Request): Promise => new Response(undefined) + }, + metrics + }) + + await expect(dclNameChecker.checkOwnership('0xb', '')).resolves.toBeFalsy() + }) + + it('when no names returned from TheGraph returns false', async () => { + const dclNameChecker = await createTheGraphDclNameChecker({ + config, + logs, + fetch: { + fetch: async (_url: Request): Promise => + new Response( + JSON.stringify({ + data: { nfts: [] } + }) + ) + }, + metrics + }) + + await expect(dclNameChecker.checkOwnership('0xb', 'my-super-name.dcl.eth')).resolves.toBeFalsy() + }) + + it('when requested name is returned from TheGraph returns true', async () => { + const dclNameChecker = await createTheGraphDclNameChecker({ + config, + logs, + fetch: { + fetch: async (_url: Request): Promise => + new Response(JSON.stringify({ data: { nfts: [{ name: 'my-super-name', owner: { id: '0xb' } }] } })) + }, + metrics + }) + await expect(dclNameChecker.checkOwnership('0xb', 'my-super-name.dcl.eth')).resolves.toBeTruthy() + }) +}) + +describe('dcl name checker: on-chain', function () { + let config: IConfigComponent + let logs: ILoggerComponent + + beforeEach(async () => { + config = createConfigComponent({ + NETWORK_ID: '1', + LOG_LEVEL: 'DEBUG', + RPC_URL: 'https://rpc-url.com' + }) + logs = await createLogComponent({ config }) + }) + + it.each(['', 'name'])('when permission asked for invalid name returns false', async (name) => { + const fetch: IFetchComponent = { + fetch: async (_url: Request): Promise => new Response(undefined) + } + + const dclNameChecker = await createOnChainDclNameChecker({ + config, + logs, + fetch + }) + + await expect(dclNameChecker.checkOwnership('0xb', name)).resolves.toBeFalsy() + }) + + it('when on chain validation returns false', async () => { + const fetch: IFetchComponent = { + fetch: async (_url: Request): Promise => + new Response( + JSON.stringify({ + jsonrpc: '2.0', + id: 1, + result: '0x0000000000000000000000000000000000000000000000000000000000000000' + }) + ) + } + + const dclNameChecker = await createOnChainDclNameChecker({ + config, + fetch, + logs + }) + + const identity = await getIdentity() + const address = identity.authChain.authChain[0].payload + await expect(dclNameChecker.checkOwnership(address, 'my-super-name.dcl.eth')).resolves.toBeFalsy() + }) + + it('when on chain validation returns true', async () => { + const fetch: IFetchComponent = { + fetch: async (_url: Request): Promise => + new Response( + JSON.stringify({ + jsonrpc: '2.0', + id: 1, + result: '0x0000000000000000000000000000000000000000000000000000000000000001' + }) + ) + } + const dclNameChecker = await createOnChainDclNameChecker({ + config, + logs, + fetch + }) + + const identity = await getIdentity() + const address = identity.authChain.authChain[0].payload + await expect(dclNameChecker.checkOwnership(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() + }) +}) + +describe('name checker: noop', function () { + let dclNameChecker: IDclNameChecker + + beforeEach(async () => { + dclNameChecker = await createNoOwnershipSupportedNameChecker() + }) + + it('when permission asked for invalid name returns false', async () => { + await expect(dclNameChecker.checkOwnership('0xb', '')).resolves.toBeFalsy() + }) + + it('when permission asked for invalid address returns false', async () => { + await expect(dclNameChecker.checkOwnership('', 'anything')).resolves.toBeFalsy() + }) + + it('when valid name and address it returns false', async () => { + const identity = await getIdentity() + const address = identity.authChain.authChain[0].payload + await expect(dclNameChecker.checkOwnership(address, 'my-super-name.dcl.eth')).resolves.toBeFalsy() + }) +}) diff --git a/test/unit/validator.spec.ts b/test/unit/validator.spec.ts index 97ca2131..b6ac7759 100644 --- a/test/unit/validator.spec.ts +++ b/test/unit/validator.spec.ts @@ -38,7 +38,6 @@ import { createLogComponent } from '@well-known-components/logger' describe('validator', function () { let config: IConfigComponent let storage: IContentStorageComponent - let ethereumProvider: HTTPProvider let fetch let limitsManager: ILimitsManager let worldNamePermissionChecker: IWorldNamePermissionChecker @@ -57,7 +56,6 @@ describe('validator', function () { } } - ethereumProvider = new HTTPProvider('http://localhost', fetch) limitsManager = createMockLimitsManagerComponent() worldNamePermissionChecker = createMockNamePermissionChecker(['whatever.dcl.eth']) worldsManager = await createWorldsManagerComponent({ @@ -70,7 +68,6 @@ describe('validator', function () { config, storage, limitsManager, - ethereumProvider, namePermissionChecker: worldNamePermissionChecker, worldsManager } diff --git a/test/unit/world-name-permission-checker.spec.ts b/test/unit/world-name-permission-checker.spec.ts index 4978a564..36f60563 100644 --- a/test/unit/world-name-permission-checker.spec.ts +++ b/test/unit/world-name-permission-checker.spec.ts @@ -1,20 +1,18 @@ import { createConfigComponent } from '@well-known-components/env-config-provider' -import { Variables } from '@well-known-components/thegraph-component/dist/types' import { + createDclNamePlusACLPermissionChecker, createEndpointNameChecker, - createNoOpNameChecker, - createOnChainDclNameChecker, - createTheGraphDclNameChecker + createNoOpNameChecker } from '../../src/adapters/world-name-permission-checker' import { createLogComponent } from '@well-known-components/logger' import { IConfigComponent, ILoggerComponent } from '@well-known-components/interfaces' import { getIdentity } from '../utils' -import { createHttpProviderMock } from '../mocks/http-provider-mock' import { IWorldNamePermissionChecker } from '../../src/types' import { IFetchComponent } from '@well-known-components/http-server' import { Request, Response } from 'node-fetch' +import { EthAddress } from '@dcl/schemas' -describe('dcl name checker: TheGraph', function () { +describe('dcl name checker + ACL', function () { let logs: ILoggerComponent beforeEach(async () => { @@ -25,26 +23,13 @@ describe('dcl name checker: TheGraph', function () { }) }) - it('when permission asked for invalid name returns false', async () => { - const permissionChecker = await createTheGraphDclNameChecker({ + it('when dcl name checker says no returns false', async () => { + const permissionChecker = await createDclNamePlusACLPermissionChecker({ logs, - marketplaceSubGraph: { - query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ - names: [] - }) - } - }) - - await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() - }) - - it('when no names returned from TheGraph returns false', async () => { - const permissionChecker = await createTheGraphDclNameChecker({ - logs, - marketplaceSubGraph: { - query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ - nfts: [] - }) + dclNameChecker: { + checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { + return Promise.resolve(false) + } } }) @@ -52,81 +37,18 @@ describe('dcl name checker: TheGraph', function () { }) it('when requested name is returned from TheGraph returns true', async () => { - const permissionChecker = await createTheGraphDclNameChecker({ + const permissionChecker = await createDclNamePlusACLPermissionChecker({ logs, - marketplaceSubGraph: { - query: async (_query: string, _variables?: Variables, _remainingAttempts?: number): Promise => ({ - nfts: [ - { - name: 'my-super-name', - owner: { - id: '0xb' - } - } - ] - }) + dclNameChecker: { + checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { + return Promise.resolve(true) + } } }) - await expect(permissionChecker.checkPermission('0xb', 'my-super-name.dcl.eth')).resolves.toBeTruthy() }) }) -describe('dcl name checker: on-chain', function () { - let logs: ILoggerComponent - let config: IConfigComponent - - beforeEach(async () => { - config = createConfigComponent({ - NETWORK_ID: '1', - LOG_LEVEL: 'DEBUG' - }) - logs = await createLogComponent({ config }) - }) - - it.each(['', 'name'])('when permission asked for invalid name returns false', async (name) => { - const permissionChecker = await createOnChainDclNameChecker({ - config, - logs, - ethereumProvider: createHttpProviderMock() - }) - - await expect(permissionChecker.checkPermission('0xb', name)).resolves.toBeFalsy() - }) - - it('when on chain validation returns false', async () => { - const permissionChecker = await createOnChainDclNameChecker({ - config, - logs, - ethereumProvider: createHttpProviderMock({ - jsonrpc: '2.0', - id: 1, - result: '0x0000000000000000000000000000000000000000000000000000000000000000' - }) - }) - - const identity = await getIdentity() - const address = identity.authChain.authChain[0].payload - await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeFalsy() - }) - - it('when on chain validation returns true', async () => { - const permissionChecker = await createOnChainDclNameChecker({ - config, - logs, - ethereumProvider: createHttpProviderMock({ - jsonrpc: '2.0', - id: 1, - result: '0x0000000000000000000000000000000000000000000000000000000000000001' - }) - }) - - const identity = await getIdentity() - const address = identity.authChain.authChain[0].payload - await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() - }) -}) - describe('name checker: endpoint', function () { let logs: ILoggerComponent let config: IConfigComponent From f03b1b5670c48b5a56e315a2052c369903bd0aeb Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Wed, 1 Mar 2023 12:21:46 -0500 Subject: [PATCH 13/19] wip --- src/adapters/validator.ts | 38 +---- src/adapters/world-name-permission-checker.ts | 39 ++++- src/components.ts | 15 +- .../handlers/deploy-entity-handler.ts | 2 +- src/types.ts | 4 +- test/components.ts | 4 +- test/integration/deploy.spec.ts | 28 ++-- test/unit/validator.spec.ts | 2 +- .../world-name-permission-checker.spec.ts | 138 +++++++++--------- 9 files changed, 142 insertions(+), 128 deletions(-) diff --git a/src/adapters/validator.ts b/src/adapters/validator.ts index 9544b540..e457d434 100644 --- a/src/adapters/validator.ts +++ b/src/adapters/validator.ts @@ -105,40 +105,16 @@ export const validateSignature: Validation = async ( } export const validateDeploymentPermission: Validation = async ( - components: Pick, + components: Pick, deployment: DeploymentToValidate ): Promise => { - const sceneJson = JSON.parse(deployment.files.get(deployment.entity.id)!.toString()) - const worldSpecifiedName = sceneJson.metadata.worldConfiguration.name - const signer = deployment.authChain[0].payload - - const hasPermission = await components.namePermissionChecker.checkPermission(signer, worldSpecifiedName) + const hasPermission = await components.permissionChecker.validate(deployment) if (!hasPermission) { - async function allowedByAcl(worldName: string, address: EthAddress): Promise { - const worldMetadata = await components.worldsManager.getMetadataForWorld(worldName) - if (!worldMetadata || !worldMetadata.acl) { - // No acl -> no permission - return false - } - - const acl = JSON.parse(worldMetadata.acl.slice(-1).pop()!.payload) as AccessControlList - const isAllowed = acl.allowed.some((allowedAddress) => allowedAddress.toLowerCase() === address.toLowerCase()) - if (!isAllowed) { - // There is acl but requested address is not included in the allowed ones - return false - } - - // The acl allows permissions, finally check that the signer of the acl still owns the world - const aclSigner = worldMetadata.acl[0].payload - return components.namePermissionChecker.checkPermission(aclSigner, worldName) - } - - const allowed = await allowedByAcl(worldSpecifiedName, signer) - if (!allowed) { - return createValidationResult([ - `Deployment failed: Your wallet has no permission to publish this scene because it does not have permission to deploy under "${worldSpecifiedName}". Check scene.json to select a name that either you own or you were given permission to deploy.` - ]) - } + const sceneJson = JSON.parse(deployment.files.get(deployment.entity.id)!.toString()) + const worldSpecifiedName = sceneJson.metadata.worldConfiguration.name + return createValidationResult([ + `Deployment failed: Your wallet has no permission to publish this scene because it does not have permission to deploy under "${worldSpecifiedName}". Check scene.json to select a name that either you own or you were given permission to deploy.` + ]) } return OK diff --git a/src/adapters/world-name-permission-checker.ts b/src/adapters/world-name-permission-checker.ts index ec2916d3..a5617cac 100644 --- a/src/adapters/world-name-permission-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -1,8 +1,8 @@ -import { AppComponents, DeploymentToValidate, IWorldNamePermissionChecker } from '../types' +import { AccessControlList, AppComponents, DeploymentToValidate, IWorldNamePermissionChecker } from '../types' import { EthAddress } from '@dcl/schemas' export async function createWorldNamePermissionChecker( - components: Pick + components: Pick ): Promise { const logger = components.logs.getLogger('check-permissions') const nameValidatorStrategy = await components.config.requireString('NAME_VALIDATOR') @@ -22,7 +22,7 @@ export async function createWorldNamePermissionChecker( } export async function createDclNamePlusACLPermissionChecker( - components: Pick + components: Pick ): Promise { return { checkPermission: async function (ethAddress: EthAddress, worldName: string): Promise { @@ -32,8 +32,37 @@ export async function createDclNamePlusACLPermissionChecker( // TODO check ACL return false }, - validate(_deployment: DeploymentToValidate): Promise { - return Promise.resolve(false) + async validate(deployment: DeploymentToValidate): Promise { + const sceneJson = JSON.parse(deployment.files.get(deployment.entity.id)!.toString()) + const worldSpecifiedName = sceneJson.metadata.worldConfiguration.name + const signer = deployment.authChain[0].payload + + const hasPermission = await components.dclNameChecker.checkOwnership(signer, worldSpecifiedName) + if (!hasPermission) { + async function allowedByAcl(worldName: string, address: EthAddress): Promise { + const worldMetadata = await components.worldsManager.getMetadataForWorld(worldName) + if (!worldMetadata || !worldMetadata.acl) { + // No acl -> no permission + return false + } + + const acl = JSON.parse(worldMetadata.acl.slice(-1).pop()!.payload) as AccessControlList + const isAllowed = acl.allowed.some((allowedAddress) => allowedAddress.toLowerCase() === address.toLowerCase()) + if (!isAllowed) { + // There is acl but requested address is not included in the allowed ones + return false + } + + // The acl allows permissions, finally check that the signer of the acl still owns the world + const aclSigner = worldMetadata.acl[0].payload + return components.dclNameChecker.checkOwnership(aclSigner, worldName) + } + + const allowed = await allowedByAcl(worldSpecifiedName, signer) + return Promise.resolve(allowed) + } + + return Promise.resolve(true) } } } diff --git a/src/components.ts b/src/components.ts index f7c6e7b3..a93705e3 100644 --- a/src/components.ts +++ b/src/components.ts @@ -64,20 +64,21 @@ export async function initComponents(): Promise { metrics }) + const limitsManager = await createLimitsManagerComponent({ config, fetch, logs }) + + const worldsManager = await createWorldsManagerComponent({ logs, storage }) + const namePermissionChecker = await createWorldNamePermissionChecker({ config, dclNameChecker, fetch, - logs + logs, + worldsManager }) - const limitsManager = await createLimitsManagerComponent({ config, fetch, logs }) - - const worldsManager = await createWorldsManagerComponent({ logs, storage }) - const validator = createValidator({ config, - namePermissionChecker, + permissionChecker: namePermissionChecker, limitsManager, storage, worldsManager @@ -87,7 +88,7 @@ export async function initComponents(): Promise { dclNameChecker, commsAdapter, config, - namePermissionChecker, + permissionChecker: namePermissionChecker, logs, server, statusChecks, diff --git a/src/controllers/handlers/deploy-entity-handler.ts b/src/controllers/handlers/deploy-entity-handler.ts index 9467e120..3f9ee159 100644 --- a/src/controllers/handlers/deploy-entity-handler.ts +++ b/src/controllers/handlers/deploy-entity-handler.ts @@ -79,7 +79,7 @@ async function storeEntity( export async function deployEntity( ctx: FormDataContext & HandlerContextWithPath< - 'config' | 'logs' | 'namePermissionChecker' | 'metrics' | 'storage' | 'sns' | 'validator', + 'config' | 'logs' | 'permissionChecker' | 'metrics' | 'storage' | 'sns' | 'validator', '/entities' > ): Promise { diff --git a/src/types.ts b/src/types.ts index 3841d06d..d2fcc74c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,7 +45,7 @@ export type ValidationResult = { export type ValidatorComponents = Pick< AppComponents, - 'config' | 'namePermissionChecker' | 'limitsManager' | 'storage' | 'worldsManager' + 'config' | 'permissionChecker' | 'limitsManager' | 'storage' | 'worldsManager' > export type Validation = ( @@ -109,7 +109,7 @@ export type BaseComponents = { dclNameChecker: IDclNameChecker commsAdapter: ICommsAdapter config: IConfigComponent - namePermissionChecker: IWorldNamePermissionChecker + permissionChecker: IWorldNamePermissionChecker logs: ILoggerComponent server: IHttpServerComponent fetch: IFetchComponent diff --git a/test/components.ts b/test/components.ts index 28a07ea2..a14a640f 100644 --- a/test/components.ts +++ b/test/components.ts @@ -48,7 +48,7 @@ async function initComponents(): Promise { const validator = createValidator({ config, storage, - namePermissionChecker, + permissionChecker: namePermissionChecker, limitsManager, worldsManager }) @@ -57,7 +57,7 @@ async function initComponents(): Promise { return { ...components, localFetch: await createLocalFetchCompoment(config), - namePermissionChecker: namePermissionChecker, + permissionChecker: namePermissionChecker, commsAdapter, fetch, limitsManager, diff --git a/test/integration/deploy.spec.ts b/test/integration/deploy.spec.ts index a1c09f6a..101e9086 100644 --- a/test/integration/deploy.spec.ts +++ b/test/integration/deploy.spec.ts @@ -11,7 +11,7 @@ import { streamToBuffer } from '@dcl/catalyst-storage/dist/content-item' test('deployment works', function ({ components, stubComponents }) { it('creates an entity and deploys it', async () => { const { config, storage } = components - const { namePermissionChecker, metrics } = stubComponents + const { permissionChecker, metrics } = stubComponents const contentClient = new ContentClient({ contentUrl: `http://${await config.requireString('HTTP_SERVER_HOST')}:${await config.requireNumber( @@ -40,7 +40,7 @@ test('deployment works', function ({ components, stubComponents }) { // Sign entity id const identity = await getIdentity() - namePermissionChecker.checkPermission + permissionChecker.checkPermission .withArgs(identity.authChain.authChain[0].payload, 'my-super-name.dcl.eth') .resolves(true) @@ -50,7 +50,7 @@ test('deployment works', function ({ components, stubComponents }) { await contentClient.deployEntity({ files, entityId, authChain }) Sinon.assert.calledWith( - namePermissionChecker.checkPermission, + permissionChecker.checkPermission, identity.authChain.authChain[0].payload, 'my-super-name.dcl.eth' ) @@ -65,7 +65,7 @@ test('deployment works', function ({ components, stubComponents }) { test('deployment works when not owner but has permission', function ({ components, stubComponents }) { it('creates an entity and deploys it', async () => { const { config, storage } = components - const { namePermissionChecker, metrics } = stubComponents + const { permissionChecker, metrics } = stubComponents const contentClient = new ContentClient({ contentUrl: `http://${await config.requireString('HTTP_SERVER_HOST')}:${await config.requireNumber( @@ -105,10 +105,10 @@ test('deployment works when not owner but has permission', function ({ component } }) - namePermissionChecker.checkPermission + permissionChecker.checkPermission .withArgs(ownerIdentity.authChain.authChain[0].payload, 'my-super-name.dcl.eth') .resolves(true) - namePermissionChecker.checkPermission + permissionChecker.checkPermission .withArgs(delegatedIdentity.authChain.authChain[0].payload, 'my-super-name.dcl.eth') .resolves(false) @@ -118,13 +118,13 @@ test('deployment works when not owner but has permission', function ({ component await contentClient.deployEntity({ files, entityId, authChain }) Sinon.assert.calledWith( - namePermissionChecker.checkPermission, + permissionChecker.checkPermission, ownerIdentity.authChain.authChain[0].payload, 'my-super-name.dcl.eth' ) Sinon.assert.calledWith( - namePermissionChecker.checkPermission, + permissionChecker.checkPermission, delegatedIdentity.authChain.authChain[0].payload, 'my-super-name.dcl.eth' ) @@ -143,7 +143,7 @@ test('deployment works when not owner but has permission', function ({ component test('deployment with failed validation', function ({ components, stubComponents }) { it('does not work because user does not own requested name', async () => { const { config, storage } = components - const { namePermissionChecker, metrics } = stubComponents + const { permissionChecker, metrics } = stubComponents const contentClient = new ContentClient({ contentUrl: `http://${await config.requireString('HTTP_SERVER_HOST')}:${await config.requireNumber( @@ -172,7 +172,7 @@ test('deployment with failed validation', function ({ components, stubComponents // Sign entity id const identity = await getIdentity() - namePermissionChecker.checkPermission + permissionChecker.checkPermission .withArgs(identity.authChain.authChain[0].payload, 'just-do-it.dcl.eth') .resolves(false) @@ -184,7 +184,7 @@ test('deployment with failed validation', function ({ components, stubComponents ) Sinon.assert.calledWith( - namePermissionChecker.checkPermission, + permissionChecker.checkPermission, identity.authChain.authChain[0].payload, 'just-do-it.dcl.eth' ) @@ -199,7 +199,7 @@ test('deployment with failed validation', function ({ components, stubComponents test('deployment with failed validation', function ({ components, stubComponents }) { it('does not work because user did not specify any names', async () => { const { config, storage } = components - const { namePermissionChecker, metrics } = stubComponents + const { permissionChecker, metrics } = stubComponents const contentClient = new ContentClient({ contentUrl: `http://${await config.requireString('HTTP_SERVER_HOST')}:${await config.requireNumber( @@ -224,7 +224,7 @@ test('deployment with failed validation', function ({ components, stubComponents // Sign entity id const identity = await getIdentity() - namePermissionChecker.checkPermission + permissionChecker.checkPermission .withArgs(identity.authChain.authChain[0].payload, 'my-super-name.dcl.eth') .resolves(false) @@ -235,7 +235,7 @@ test('deployment with failed validation', function ({ components, stubComponents 'Deployment failed: scene.json needs to specify a worldConfiguration section with a valid name inside.' ) - Sinon.assert.notCalled(namePermissionChecker.checkPermission) + Sinon.assert.notCalled(permissionChecker.checkPermission) expect(await storage.exist(fileHash)).toEqual(false) expect(await storage.exist(entityId)).toEqual(false) diff --git a/test/unit/validator.spec.ts b/test/unit/validator.spec.ts index b6ac7759..8626218a 100644 --- a/test/unit/validator.spec.ts +++ b/test/unit/validator.spec.ts @@ -68,7 +68,7 @@ describe('validator', function () { config, storage, limitsManager, - namePermissionChecker: worldNamePermissionChecker, + permissionChecker: worldNamePermissionChecker, worldsManager } }) diff --git a/test/unit/world-name-permission-checker.spec.ts b/test/unit/world-name-permission-checker.spec.ts index 36f60563..47da246d 100644 --- a/test/unit/world-name-permission-checker.spec.ts +++ b/test/unit/world-name-permission-checker.spec.ts @@ -7,13 +7,18 @@ import { import { createLogComponent } from '@well-known-components/logger' import { IConfigComponent, ILoggerComponent } from '@well-known-components/interfaces' import { getIdentity } from '../utils' -import { IWorldNamePermissionChecker } from '../../src/types' +import { IWorldNamePermissionChecker, IWorldsManager } from '../../src/types' import { IFetchComponent } from '@well-known-components/http-server' import { Request, Response } from 'node-fetch' import { EthAddress } from '@dcl/schemas' +import { createWorldsManagerComponent } from '../../src/adapters/worlds-manager' +import { createInMemoryStorage } from '@dcl/catalyst-storage' +import { IContentStorageComponent } from '@dcl/catalyst-storage/dist/types' describe('dcl name checker + ACL', function () { let logs: ILoggerComponent + let storage: IContentStorageComponent + let worldsManager: IWorldsManager beforeEach(async () => { logs = await createLogComponent({ @@ -21,11 +26,14 @@ describe('dcl name checker + ACL', function () { LOG_LEVEL: 'DEBUG' }) }) + storage = createInMemoryStorage() + worldsManager = await createWorldsManagerComponent({ logs, storage }) }) it('when dcl name checker says no returns false', async () => { const permissionChecker = await createDclNamePlusACLPermissionChecker({ logs, + worldsManager, dclNameChecker: { checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { return Promise.resolve(false) @@ -33,74 +41,74 @@ describe('dcl name checker + ACL', function () { } }) - await expect(permissionChecker.checkPermission('0xb', 'my-super-name.dcl.eth')).resolves.toBeFalsy() + await expect(permissionChecker.validate('0xb', 'my-super-name.dcl.eth')).resolves.toBeFalsy() }) - it('when requested name is returned from TheGraph returns true', async () => { - const permissionChecker = await createDclNamePlusACLPermissionChecker({ - logs, - dclNameChecker: { - checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { - return Promise.resolve(true) - } - } - }) - await expect(permissionChecker.checkPermission('0xb', 'my-super-name.dcl.eth')).resolves.toBeTruthy() - }) + // it('when requested name is returned from TheGraph returns true', async () => { + // const permissionChecker = await createDclNamePlusACLPermissionChecker({ + // logs, + // dclNameChecker: { + // checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { + // return Promise.resolve(true) + // } + // } + // }) + // await expect(permissionChecker.checkPermission('0xb', 'my-super-name.dcl.eth')).resolves.toBeTruthy() + // }) }) -describe('name checker: endpoint', function () { - let logs: ILoggerComponent - let config: IConfigComponent - let fetch: IFetchComponent - - beforeEach(async () => { - config = createConfigComponent({ - LOG_LEVEL: 'DEBUG', - ENDPOINT_NAME_CHECKER_BASE_URL: 'http://anything' - }) - logs = await createLogComponent({ config }) - fetch = { - fetch: async (_url: Request): Promise => new Response(undefined) - } - }) - - it('when permission asked for invalid name returns false', async () => { - const permissionChecker = await createEndpointNameChecker({ - config, - fetch, - logs - }) - - await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() - }) - - it('when permission asked for invalid address returns false', async () => { - const permissionChecker = await createEndpointNameChecker({ - config, - fetch, - logs - }) - - await expect(permissionChecker.checkPermission('', 'anything')).resolves.toBeFalsy() - }) - - it.each([true, false])('when valid name and address it returns as per the endpoint', async (value) => { - fetch = { - fetch: async (_url: Request): Promise => new Response(String(value)) - } - - const permissionChecker = await createEndpointNameChecker({ - config, - fetch, - logs - }) - - const identity = await getIdentity() - const address = identity.authChain.authChain[0].payload - await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBe(value) - }) -}) +// describe('name checker: endpoint', function () { +// let logs: ILoggerComponent +// let config: IConfigComponent +// let fetch: IFetchComponent +// +// beforeEach(async () => { +// config = createConfigComponent({ +// LOG_LEVEL: 'DEBUG', +// ENDPOINT_NAME_CHECKER_BASE_URL: 'http://anything' +// }) +// logs = await createLogComponent({ config }) +// fetch = { +// fetch: async (_url: Request): Promise => new Response(undefined) +// } +// }) +// +// it('when permission asked for invalid name returns false', async () => { +// const permissionChecker = await createEndpointNameChecker({ +// config, +// fetch, +// logs +// }) +// +// await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() +// }) +// +// it('when permission asked for invalid address returns false', async () => { +// const permissionChecker = await createEndpointNameChecker({ +// config, +// fetch, +// logs +// }) +// +// await expect(permissionChecker.checkPermission('', 'anything')).resolves.toBeFalsy() +// }) +// +// it.each([true, false])('when valid name and address it returns as per the endpoint', async (value) => { +// fetch = { +// fetch: async (_url: Request): Promise => new Response(String(value)) +// } +// +// const permissionChecker = await createEndpointNameChecker({ +// config, +// fetch, +// logs +// }) +// +// const identity = await getIdentity() +// const address = identity.authChain.authChain[0].payload +// await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBe(value) +// }) +// }) describe('name checker: noop', function () { let permissionChecker: IWorldNamePermissionChecker From fd50a35b76b72b92bb830e8ceef220465d765fdf Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Thu, 2 Mar 2023 11:44:03 -0500 Subject: [PATCH 14/19] remove validateSdkVersion --- src/adapters/validator.ts | 24 +----------------------- test/unit/validator.spec.ts | 31 +------------------------------ 2 files changed, 2 insertions(+), 53 deletions(-) diff --git a/src/adapters/validator.ts b/src/adapters/validator.ts index e457d434..276ec37b 100644 --- a/src/adapters/validator.ts +++ b/src/adapters/validator.ts @@ -217,24 +217,6 @@ export const validateSize: Validation = async ( return createValidationResult(errors) } -export const validateSdkVersion: Validation = async ( - components: Pick, - deployment: DeploymentToValidate -): Promise => { - const sceneJson = JSON.parse(deployment.files.get(deployment.entity.id)!.toString()) - const worldName = sceneJson.metadata.worldConfiguration.name - const allowSdk6 = await components.limitsManager.getAllowSdk6For(worldName || '') - - const sdkVersion = deployment.entity.metadata.runtimeVersion - if (sdkVersion !== '7' && !allowSdk6) { - return createValidationResult([ - `Worlds are only supported on SDK 7. Please upgrade your scene to latest version of SDK.` - ]) - } - - return OK -} - const mandatoryValidations: Validation[] = [ validateEntity, validateEntityId, @@ -246,11 +228,7 @@ const mandatoryValidations: Validation[] = [ validateDeploymentPermission ] -const optionalValidations: Validation[] = [ - validateSceneDimensions, - validateSize - // validateSdkVersion TODO re-enable (and test) once SDK7 is ready -] +const optionalValidations: Validation[] = [validateSceneDimensions, validateSize] const allValidations: Validation[] = [...mandatoryValidations, ...optionalValidations] diff --git a/test/unit/validator.spec.ts b/test/unit/validator.spec.ts index 8626218a..19f5bb31 100644 --- a/test/unit/validator.spec.ts +++ b/test/unit/validator.spec.ts @@ -8,7 +8,6 @@ import { validateEntityId, validateFiles, validateSceneDimensions, - validateSdkVersion, validateSignature, validateSigner, validateSize @@ -21,7 +20,7 @@ import { ValidatorComponents, IWorldsManager } from '../../src/types' -import { HTTPProvider, stringToUtf8Bytes } from 'eth-connect' +import { stringToUtf8Bytes } from 'eth-connect' import { EntityType } from '@dcl/schemas' import { createMockLimitsManagerComponent } from '../mocks/limits-manager-mock' import { createMockNamePermissionChecker } from '../mocks/world-name-permission-checker-mock' @@ -38,7 +37,6 @@ import { createLogComponent } from '@well-known-components/logger' describe('validator', function () { let config: IConfigComponent let storage: IContentStorageComponent - let fetch let limitsManager: ILimitsManager let worldNamePermissionChecker: IWorldNamePermissionChecker let worldsManager: IWorldsManager @@ -50,12 +48,6 @@ describe('validator', function () { DEPLOYMENT_TTL: '10000' }) storage = createInMemoryStorage() - fetch = { - fetch: (_url: string, _params: { body?: any; method?: string; mode?: string; headers?: any }): Promise => { - return Promise.resolve({}) - } - } - limitsManager = createMockLimitsManagerComponent() worldNamePermissionChecker = createMockNamePermissionChecker(['whatever.dcl.eth']) worldsManager = await createWorldsManagerComponent({ @@ -270,27 +262,6 @@ describe('validator', function () { 'The deployment is too big. The maximum total size allowed is 10 MB for scenes. You can upload up to 10485760 bytes but you tried to upload 10485763.' ) }) - - it('validateSdkVersion with errors', async () => { - const deployment = await createDeployment(identity.authChain, { - type: EntityType.SCENE, - pointers: ['0,0'], - timestamp: Date.now(), - metadata: { - runtimeVersion: '6', - worldConfiguration: { - name: 'whatever.dcl.eth' - } - }, - files: [] - }) - - const result = await validateSdkVersion(components, deployment) - expect(result.ok()).toBeFalsy() - expect(result.errors).toContain( - 'Worlds are only supported on SDK 7. Please upgrade your scene to latest version of SDK.' - ) - }) }) async function createDeployment(identityAuthChain: AuthIdentity, entity?: any) { From a978cc05c7f8c4ff33d448ffae3ed4dcf2707129 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Thu, 2 Mar 2023 12:07:07 -0500 Subject: [PATCH 15/19] tests for NoOpNameChecker --- test/unit/validator.spec.ts | 48 ++---------------- .../world-name-permission-checker.spec.ts | 49 ++++++++++++++----- test/utils.ts | 42 +++++++++++++++- 3 files changed, 79 insertions(+), 60 deletions(-) diff --git a/test/unit/validator.spec.ts b/test/unit/validator.spec.ts index 19f5bb31..e3229d17 100644 --- a/test/unit/validator.spec.ts +++ b/test/unit/validator.spec.ts @@ -13,23 +13,15 @@ import { validateSize } from '../../src/adapters/validator' import { createInMemoryStorage, IContentStorageComponent } from '@dcl/catalyst-storage' -import { - DeploymentToValidate, - IWorldNamePermissionChecker, - ILimitsManager, - ValidatorComponents, - IWorldsManager -} from '../../src/types' +import { ILimitsManager, IWorldNamePermissionChecker, IWorldsManager, ValidatorComponents } from '../../src/types' import { stringToUtf8Bytes } from 'eth-connect' import { EntityType } from '@dcl/schemas' import { createMockLimitsManagerComponent } from '../mocks/limits-manager-mock' import { createMockNamePermissionChecker } from '../mocks/world-name-permission-checker-mock' -import { DeploymentBuilder } from 'dcl-catalyst-client' -import { getIdentity } from '../utils' -import { Authenticator, AuthIdentity } from '@dcl/crypto' +import { createDeployment, getIdentity } from '../utils' +import { Authenticator } from '@dcl/crypto' import { IConfigComponent } from '@well-known-components/interfaces' import { hashV0, hashV1 } from '@dcl/hashing' -import { TextDecoder } from 'util' import { bufferToStream } from '@dcl/catalyst-storage/dist/content-item' import { createWorldsManagerComponent } from '../../src/adapters/worlds-manager' import { createLogComponent } from '@well-known-components/logger' @@ -263,37 +255,3 @@ describe('validator', function () { ) }) }) - -async function createDeployment(identityAuthChain: AuthIdentity, entity?: any) { - const entityFiles = new Map() - entityFiles.set('abc.txt', Buffer.from(stringToUtf8Bytes('asd'))) - const fileHash = await hashV1(entityFiles.get('abc.txt')) - - const sceneJson = entity || { - type: EntityType.SCENE, - pointers: ['0,0'], - timestamp: Date.now(), - metadata: { runtimeVersion: '7', worldConfiguration: { name: 'whatever.dcl.eth' } }, - files: entityFiles - } - const { files, entityId } = await DeploymentBuilder.buildEntity(sceneJson) - files.set(entityId, Buffer.from(files.get(entityId))) - - const authChain = Authenticator.signPayload(identityAuthChain, entityId) - - const contentHashesInStorage = new Map() - contentHashesInStorage.set(fileHash, false) - - const finalEntity = { - id: entityId, - ...JSON.parse(new TextDecoder().decode(files.get(entityId))) - } - - const deployment: DeploymentToValidate = { - entity: finalEntity, - files, - authChain, - contentHashesInStorage - } - return deployment -} diff --git a/test/unit/world-name-permission-checker.spec.ts b/test/unit/world-name-permission-checker.spec.ts index 47da246d..b99e3c45 100644 --- a/test/unit/world-name-permission-checker.spec.ts +++ b/test/unit/world-name-permission-checker.spec.ts @@ -6,11 +6,11 @@ import { } from '../../src/adapters/world-name-permission-checker' import { createLogComponent } from '@well-known-components/logger' import { IConfigComponent, ILoggerComponent } from '@well-known-components/interfaces' -import { getIdentity } from '../utils' +import { createDeployment, getIdentity } from '../utils' import { IWorldNamePermissionChecker, IWorldsManager } from '../../src/types' import { IFetchComponent } from '@well-known-components/http-server' import { Request, Response } from 'node-fetch' -import { EthAddress } from '@dcl/schemas' +import { EntityType, EthAddress } from '@dcl/schemas' import { createWorldsManagerComponent } from '../../src/adapters/worlds-manager' import { createInMemoryStorage } from '@dcl/catalyst-storage' import { IContentStorageComponent } from '@dcl/catalyst-storage/dist/types' @@ -41,7 +41,7 @@ describe('dcl name checker + ACL', function () { } }) - await expect(permissionChecker.validate('0xb', 'my-super-name.dcl.eth')).resolves.toBeFalsy() + // await expect(permissionChecker.validate('0xb', 'my-super-name.dcl.eth')).resolves.toBeFalsy() }) // it('when requested name is returned from TheGraph returns true', async () => { @@ -110,24 +110,47 @@ describe('dcl name checker + ACL', function () { // }) // }) -describe('name checker: noop', function () { +describe('name checker: noop', () => { let permissionChecker: IWorldNamePermissionChecker + let identity beforeEach(async () => { permissionChecker = await createNoOpNameChecker() + identity = await getIdentity() }) - it('when permission asked for invalid name returns false', async () => { - await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() - }) + describe('checkPermission', () => { + it('when permission asked for invalid name returns false', async () => { + await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() + }) - it('when permission asked for invalid address returns false', async () => { - await expect(permissionChecker.checkPermission('', 'anything')).resolves.toBeFalsy() + it('when permission asked for invalid address returns false', async () => { + await expect(permissionChecker.checkPermission('', 'anything')).resolves.toBeFalsy() + }) + + it('when valid name and address it returns true', async () => { + const identity = await getIdentity() + const address = identity.authChain.authChain[0].payload + await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() + }) }) - it('when valid name and address it returns true', async () => { - const identity = await getIdentity() - const address = identity.authChain.authChain[0].payload - await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() + describe('validate', () => { + it('when permission asked for invalid name returns false', async () => { + const deployment = await createDeployment(identity.authChain, { + type: EntityType.SCENE, + pointers: ['0,0'], + timestamp: Date.parse('2022-11-01T00:00:00Z'), + metadata: { worldConfiguration: { name: '' } }, + files: [] + }) + + await expect(permissionChecker.validate(deployment)).resolves.toBeTruthy() + }) + + it('when valid name and address it returns true', async () => { + const deployment = await createDeployment(identity.authChain) + await expect(permissionChecker.validate(deployment)).resolves.toBeTruthy() + }) }) }) diff --git a/test/utils.ts b/test/utils.ts index 00c20c30..5b5fb5b2 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,14 +1,18 @@ import { createUnsafeIdentity } from '@dcl/crypto/dist/crypto' -import { Authenticator } from '@dcl/crypto' +import { Authenticator, AuthIdentity } from '@dcl/crypto' import { Readable } from 'stream' import { IContentStorageComponent } from '@dcl/catalyst-storage' import { stringToUtf8Bytes } from 'eth-connect' -import { AuthChain } from '@dcl/schemas' +import { AuthChain, EntityType } from '@dcl/schemas' import { AUTH_CHAIN_HEADER_PREFIX, AUTH_METADATA_HEADER, AUTH_TIMESTAMP_HEADER } from 'decentraland-crypto-middleware/lib/types' +import { hashV1 } from '@dcl/hashing' +import { DeploymentBuilder } from 'dcl-catalyst-client' +import { TextDecoder } from 'util' +import { DeploymentToValidate } from '../src/types' export async function storeJson(storage: IContentStorageComponent, fileId: string, data: any) { const buffer = stringToUtf8Bytes(JSON.stringify(data)) @@ -69,3 +73,37 @@ export function getAuthHeaders( return headers } + +export async function createDeployment(identityAuthChain: AuthIdentity, entity?: any) { + const entityFiles = new Map() + entityFiles.set('abc.txt', Buffer.from(stringToUtf8Bytes('asd'))) + const fileHash = await hashV1(entityFiles.get('abc.txt')) + + const sceneJson = entity || { + type: EntityType.SCENE, + pointers: ['0,0'], + timestamp: Date.now(), + metadata: { runtimeVersion: '7', worldConfiguration: { name: 'whatever.dcl.eth' } }, + files: entityFiles + } + const { files, entityId } = await DeploymentBuilder.buildEntity(sceneJson) + files.set(entityId, Buffer.from(files.get(entityId))) + + const authChain = Authenticator.signPayload(identityAuthChain, entityId) + + const contentHashesInStorage = new Map() + contentHashesInStorage.set(fileHash, false) + + const finalEntity = { + id: entityId, + ...JSON.parse(new TextDecoder().decode(files.get(entityId))) + } + + const deployment: DeploymentToValidate = { + entity: finalEntity, + files, + authChain, + contentHashesInStorage + } + return deployment +} From 91d382a07bb03d520c1b0ef940f394b0d3d2fecd Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Thu, 2 Mar 2023 12:48:29 -0500 Subject: [PATCH 16/19] tests for DclNamePlusACLPermissionChecker --- src/adapters/world-name-permission-checker.ts | 67 +++++++------ .../world-name-permission-checker.spec.ts | 97 +++++++++++++++---- 2 files changed, 109 insertions(+), 55 deletions(-) diff --git a/src/adapters/world-name-permission-checker.ts b/src/adapters/world-name-permission-checker.ts index a5617cac..91236202 100644 --- a/src/adapters/world-name-permission-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -24,46 +24,45 @@ export async function createWorldNamePermissionChecker( export async function createDclNamePlusACLPermissionChecker( components: Pick ): Promise { - return { - checkPermission: async function (ethAddress: EthAddress, worldName: string): Promise { - if (await components.dclNameChecker.checkOwnership(ethAddress, worldName)) { - return true - } - // TODO check ACL + async function allowedByAcl(worldName: string, address: EthAddress): Promise { + const worldMetadata = await components.worldsManager.getMetadataForWorld(worldName) + if (!worldMetadata || !worldMetadata.acl) { + // No acl -> no permission return false - }, - async validate(deployment: DeploymentToValidate): Promise { - const sceneJson = JSON.parse(deployment.files.get(deployment.entity.id)!.toString()) - const worldSpecifiedName = sceneJson.metadata.worldConfiguration.name - const signer = deployment.authChain[0].payload + } + + const acl = JSON.parse(worldMetadata.acl.slice(-1).pop()!.payload) as AccessControlList + const isAllowed = acl.allowed.some((allowedAddress) => allowedAddress.toLowerCase() === address.toLowerCase()) + if (!isAllowed) { + // There is acl but requested address is not included in the allowed ones + return false + } + + // The acl allows permissions, finally check that the signer of the acl still owns the world + const aclSigner = worldMetadata.acl[0].payload + return components.dclNameChecker.checkOwnership(aclSigner, worldName) + } - const hasPermission = await components.dclNameChecker.checkOwnership(signer, worldSpecifiedName) - if (!hasPermission) { - async function allowedByAcl(worldName: string, address: EthAddress): Promise { - const worldMetadata = await components.worldsManager.getMetadataForWorld(worldName) - if (!worldMetadata || !worldMetadata.acl) { - // No acl -> no permission - return false - } + async function checkPermission(ethAddress: EthAddress, worldName: string): Promise { + const hasPermission = await components.dclNameChecker.checkOwnership(ethAddress, worldName) + if (hasPermission) { + return true + } - const acl = JSON.parse(worldMetadata.acl.slice(-1).pop()!.payload) as AccessControlList - const isAllowed = acl.allowed.some((allowedAddress) => allowedAddress.toLowerCase() === address.toLowerCase()) - if (!isAllowed) { - // There is acl but requested address is not included in the allowed ones - return false - } + return await allowedByAcl(worldName, ethAddress) + } - // The acl allows permissions, finally check that the signer of the acl still owns the world - const aclSigner = worldMetadata.acl[0].payload - return components.dclNameChecker.checkOwnership(aclSigner, worldName) - } + async function validate(deployment: DeploymentToValidate): Promise { + const sceneJson = JSON.parse(deployment.files.get(deployment.entity.id)!.toString()) + const worldSpecifiedName = sceneJson.metadata.worldConfiguration.name + const signer = deployment.authChain[0].payload - const allowed = await allowedByAcl(worldSpecifiedName, signer) - return Promise.resolve(allowed) - } + return await checkPermission(signer, worldSpecifiedName) + } - return Promise.resolve(true) - } + return { + checkPermission, + validate } } diff --git a/test/unit/world-name-permission-checker.spec.ts b/test/unit/world-name-permission-checker.spec.ts index b99e3c45..7d7f1f49 100644 --- a/test/unit/world-name-permission-checker.spec.ts +++ b/test/unit/world-name-permission-checker.spec.ts @@ -6,7 +6,7 @@ import { } from '../../src/adapters/world-name-permission-checker' import { createLogComponent } from '@well-known-components/logger' import { IConfigComponent, ILoggerComponent } from '@well-known-components/interfaces' -import { createDeployment, getIdentity } from '../utils' +import { createDeployment, getIdentity, storeJson } from '../utils' import { IWorldNamePermissionChecker, IWorldsManager } from '../../src/types' import { IFetchComponent } from '@well-known-components/http-server' import { Request, Response } from 'node-fetch' @@ -14,11 +14,13 @@ import { EntityType, EthAddress } from '@dcl/schemas' import { createWorldsManagerComponent } from '../../src/adapters/worlds-manager' import { createInMemoryStorage } from '@dcl/catalyst-storage' import { IContentStorageComponent } from '@dcl/catalyst-storage/dist/types' +import { Authenticator } from '@dcl/crypto' describe('dcl name checker + ACL', function () { let logs: ILoggerComponent let storage: IContentStorageComponent let worldsManager: IWorldsManager + let identity beforeEach(async () => { logs = await createLogComponent({ @@ -28,33 +30,86 @@ describe('dcl name checker + ACL', function () { }) storage = createInMemoryStorage() worldsManager = await createWorldsManagerComponent({ logs, storage }) + identity = await getIdentity() }) - it('when dcl name checker says no returns false', async () => { - const permissionChecker = await createDclNamePlusACLPermissionChecker({ - logs, - worldsManager, - dclNameChecker: { - checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { - return Promise.resolve(false) + describe('checkPermission', () => { + it('when dcl name checker says yes, returns true', async () => { + const permissionChecker = await createDclNamePlusACLPermissionChecker({ + logs, + worldsManager, + dclNameChecker: { + checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { + return Promise.resolve(true) + } } - } + }) + + await expect(permissionChecker.checkPermission('0xa', 'my-super-name.dcl.eth')).resolves.toBeTruthy() }) - // await expect(permissionChecker.validate('0xb', 'my-super-name.dcl.eth')).resolves.toBeFalsy() + it('when dcl name checker says no and no ACL configured returns false', async () => { + const permissionChecker = await createDclNamePlusACLPermissionChecker({ + logs, + worldsManager, + dclNameChecker: { + checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { + return Promise.resolve(false) + } + } + }) + + await expect(permissionChecker.checkPermission('0xa', 'my-super-name.dcl.eth')).resolves.toBeFalsy() + }) + + it('when dcl name checker says no and ACL configured it honors ACL', async () => { + const delegatedIdentity = await getIdentity() + + const payload = `{"resource":"my-world.dcl.eth","allowed":["${delegatedIdentity.realAccount.address}"]}` + + await storeJson(storage, 'name-my-world.dcl.eth', { + entityId: 'bafkreiax5plaxze77tnjbnozga7dsbefdh53horza4adf2xjzxo3k5i4xq', + acl: Authenticator.signPayload(identity.authChain, payload) + }) + + const permissionChecker = await createDclNamePlusACLPermissionChecker({ + logs, + worldsManager, + dclNameChecker: { + checkOwnership(ethAddress: EthAddress, _worldName: string): Promise { + return Promise.resolve(identity.realAccount.address.toLowerCase() === ethAddress.toLowerCase()) + } + } + }) + + // Rejects address not included in ACL + await expect(permissionChecker.checkPermission('0xa', 'my-world.dcl.eth')).resolves.toBeFalsy() + + // Accepts address included in ACL + await expect( + permissionChecker.checkPermission(delegatedIdentity.authChain.authChain[0].payload, 'my-world.dcl.eth') + ).resolves.toBeTruthy() + }) }) - // it('when requested name is returned from TheGraph returns true', async () => { - // const permissionChecker = await createDclNamePlusACLPermissionChecker({ - // logs, - // dclNameChecker: { - // checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { - // return Promise.resolve(true) - // } - // } - // }) - // await expect(permissionChecker.checkPermission('0xb', 'my-super-name.dcl.eth')).resolves.toBeTruthy() - // }) + describe('validate', () => { + it.each([false, true])('returns same value as checkPermission', async (expected) => { + const permissionChecker = await createDclNamePlusACLPermissionChecker({ + logs, + worldsManager, + dclNameChecker: { + checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { + return Promise.resolve(expected) + } + } + }) + + const deployment = await createDeployment(identity.authChain) + await expect(permissionChecker.validate(deployment)).resolves.toBe( + await permissionChecker.checkPermission(identity.realAccount.address, 'my-world.dcl.eth') + ) + }) + }) }) // describe('name checker: endpoint', function () { From a2916f9c1437c3d7063180db44fbb0cb40df7e9e Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Thu, 2 Mar 2023 15:23:48 -0500 Subject: [PATCH 17/19] big refactor --- src/adapters/validator.ts | 9 +- src/adapters/world-name-permission-checker.ts | 58 +++-- src/types.ts | 8 +- test/components.ts | 1 - test/integration/deploy.spec.ts | 109 +------- test/mocks/http-provider-mock.ts | 13 - test/mocks/marketplace-subgraph-mock.ts | 11 - .../world-name-permission-checker-mock.ts | 6 +- test/unit/dcl-name-checker.spec.ts | 1 - .../world-name-permission-checker.spec.ts | 232 +++++++++++++----- 10 files changed, 227 insertions(+), 221 deletions(-) delete mode 100644 test/mocks/http-provider-mock.ts delete mode 100644 test/mocks/marketplace-subgraph-mock.ts diff --git a/src/adapters/validator.ts b/src/adapters/validator.ts index 276ec37b..08f8232d 100644 --- a/src/adapters/validator.ts +++ b/src/adapters/validator.ts @@ -1,11 +1,4 @@ -import { - AccessControlList, - DeploymentToValidate, - Validation, - ValidationResult, - Validator, - ValidatorComponents -} from '../types' +import { DeploymentToValidate, Validation, ValidationResult, Validator, ValidatorComponents } from '../types' import { AuthChain, Entity, EthAddress, IPFSv2 } from '@dcl/schemas' import { Authenticator } from '@dcl/crypto' import { hashV1 } from '@dcl/hashing' diff --git a/src/adapters/world-name-permission-checker.ts b/src/adapters/world-name-permission-checker.ts index 91236202..7c3b3cbf 100644 --- a/src/adapters/world-name-permission-checker.ts +++ b/src/adapters/world-name-permission-checker.ts @@ -22,7 +22,7 @@ export async function createWorldNamePermissionChecker( } export async function createDclNamePlusACLPermissionChecker( - components: Pick + components: Pick ): Promise { async function allowedByAcl(worldName: string, address: EthAddress): Promise { const worldMetadata = await components.worldsManager.getMetadataForWorld(worldName) @@ -67,29 +67,48 @@ export async function createDclNamePlusACLPermissionChecker( } export async function createEndpointNameChecker( - components: Pick + components: Pick ): Promise { - const nameCheckUrl = await components.config.requireString('ENDPOINT_NAME_CHECKER_BASE_URL') + const baseUrl = await components.config.requireString('ENDPOINT_NAME_CHECKER_BASE_URL') + const permissionUrl = (baseUrl.endsWith('/') ? baseUrl : baseUrl + '/') + 'permission' + const validateUrl = (baseUrl.endsWith('/') ? baseUrl : baseUrl + '/') + 'validate' - return { - checkPermission: async (ethAddress: EthAddress, worldName: string): Promise => { - if (worldName.length === 0 || ethAddress.length === 0) { - return false - } - - const res = await components.fetch.fetch(nameCheckUrl, { - method: 'POST', - body: JSON.stringify({ - worldName: worldName, - ethAddress: ethAddress - }) + async function checkPermission(ethAddress: EthAddress, worldName: string): Promise { + if (worldName.length === 0 || ethAddress.length === 0) { + return false + } + + const res = await components.fetch.fetch(permissionUrl, { + method: 'POST', + body: JSON.stringify({ + worldName: worldName, + ethAddress: ethAddress }) + }) - return res.json() - }, - validate(_deployment: DeploymentToValidate): Promise { - return Promise.resolve(false) + return res.json() + } + + async function validate(deployment: DeploymentToValidate): Promise { + const sceneJson = JSON.parse(deployment.files.get(deployment.entity.id)!.toString()) + const worldName = sceneJson.metadata.worldConfiguration.name + const ethAddress = deployment.authChain[0].payload + + if (worldName.length === 0 || ethAddress.length === 0) { + return false } + + const res = await components.fetch.fetch(validateUrl, { + method: 'POST', + body: JSON.stringify(sceneJson) + }) + + return res.json() + } + + return { + checkPermission, + validate } } @@ -97,6 +116,7 @@ export async function createNoOpNameChecker(): Promise { return !(worldName.length === 0 || ethAddress.length === 0) } + return { checkPermission, validate(_deployment: DeploymentToValidate): Promise { diff --git a/src/types.ts b/src/types.ts index d2fcc74c..25a6e82f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,15 +1,13 @@ import type { IFetchComponent } from '@well-known-components/http-server' import type { + IBaseComponent, IConfigComponent, - ILoggerComponent, IHttpServerComponent, - IBaseComponent, + ILoggerComponent, IMetricsComponent } from '@well-known-components/interfaces' import { metricDeclarations } from './metrics' import { IContentStorageComponent } from '@dcl/catalyst-storage' -import { HTTPProvider } from 'eth-connect' -import { ISubgraphComponent } from '@well-known-components/thegraph-component' import { IStatusComponent } from './adapters/status' import { AuthChain, Entity, EthAddress } from '@dcl/schemas' @@ -145,5 +143,3 @@ export type HandlerContextWithPath< }>, Path > - -export type Context = IHttpServerComponent.PathAwareContext diff --git a/test/components.ts b/test/components.ts index a14a640f..57d95f23 100644 --- a/test/components.ts +++ b/test/components.ts @@ -6,7 +6,6 @@ import { createLocalFetchCompoment, createRunner } from '@well-known-components/ import { main } from '../src/service' import { TestComponents } from '../src/types' import { initComponents as originalInitComponents } from '../src/components' -import { createMockMarketplaceSubGraph } from './mocks/marketplace-subgraph-mock' import { createMockNamePermissionChecker } from './mocks/world-name-permission-checker-mock' import { createValidator } from '../src/adapters/validator' import { createFetchComponent } from '../src/adapters/fetch' diff --git a/test/integration/deploy.spec.ts b/test/integration/deploy.spec.ts index 101e9086..d3c3eed3 100644 --- a/test/integration/deploy.spec.ts +++ b/test/integration/deploy.spec.ts @@ -5,8 +5,7 @@ import { Authenticator } from '@dcl/crypto' import Sinon from 'sinon' import { stringToUtf8Bytes } from 'eth-connect' import { hashV1 } from '@dcl/hashing' -import { getIdentity, storeJson } from '../utils' -import { streamToBuffer } from '@dcl/catalyst-storage/dist/content-item' +import { getIdentity } from '../utils' test('deployment works', function ({ components, stubComponents }) { it('creates an entity and deploys it', async () => { @@ -40,101 +39,17 @@ test('deployment works', function ({ components, stubComponents }) { // Sign entity id const identity = await getIdentity() - permissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-super-name.dcl.eth') - .resolves(true) + permissionChecker.validate.resolves(true) const authChain = Authenticator.signPayload(identity.authChain, entityId) // Deploy entity await contentClient.deployEntity({ files, entityId, authChain }) - Sinon.assert.calledWith( - permissionChecker.checkPermission, - identity.authChain.authChain[0].payload, - 'my-super-name.dcl.eth' - ) - - expect(await storage.exist(fileHash)).toEqual(true) - expect(await storage.exist(entityId)).toEqual(true) - - Sinon.assert.calledWithMatch(metrics.increment, 'world_deployments_counter') - }) -}) - -test('deployment works when not owner but has permission', function ({ components, stubComponents }) { - it('creates an entity and deploys it', async () => { - const { config, storage } = components - const { permissionChecker, metrics } = stubComponents - - const contentClient = new ContentClient({ - contentUrl: `http://${await config.requireString('HTTP_SERVER_HOST')}:${await config.requireNumber( - 'HTTP_SERVER_PORT' - )}` - }) - - const delegatedIdentity = await getIdentity() - const ownerIdentity = await getIdentity() - - const payload = `{"resource":"my-world.dcl.eth","allowed":["${delegatedIdentity.realAccount.address}"]}` - - await storeJson(storage, 'name-my-super-name.dcl.eth', { - entityId: 'bafkreiax5plaxze77tnjbnozga7dsbefdh53horza4adf2xjzxo3k5i4xq', - acl: Authenticator.signPayload(ownerIdentity.authChain, payload) - }) - - const entityFiles = new Map() - entityFiles.set('abc.txt', stringToUtf8Bytes('asd')) - const fileHash = await hashV1(entityFiles.get('abc.txt')) - await storeJson(storage, fileHash, {}) - - await storeJson(storage, 'name-my-super-name.dcl.eth', { - entityId: fileHash, - acl: Authenticator.signPayload(ownerIdentity.authChain, payload) - }) - - // Build the entity - const { files, entityId } = await contentClient.buildEntity({ - type: EntityType.SCENE, - pointers: ['0,0'], - files: entityFiles, - metadata: { - worldConfiguration: { - name: 'my-super-name.dcl.eth' - } - } - }) - - permissionChecker.checkPermission - .withArgs(ownerIdentity.authChain.authChain[0].payload, 'my-super-name.dcl.eth') - .resolves(true) - permissionChecker.checkPermission - .withArgs(delegatedIdentity.authChain.authChain[0].payload, 'my-super-name.dcl.eth') - .resolves(false) - - const authChain = Authenticator.signPayload(delegatedIdentity.authChain, entityId) - - // Deploy entity - await contentClient.deployEntity({ files, entityId, authChain }) - - Sinon.assert.calledWith( - permissionChecker.checkPermission, - ownerIdentity.authChain.authChain[0].payload, - 'my-super-name.dcl.eth' - ) - - Sinon.assert.calledWith( - permissionChecker.checkPermission, - delegatedIdentity.authChain.authChain[0].payload, - 'my-super-name.dcl.eth' - ) + Sinon.assert.calledOnce(permissionChecker.validate) expect(await storage.exist(fileHash)).toEqual(true) expect(await storage.exist(entityId)).toEqual(true) - const content = await storage.retrieve('name-my-super-name.dcl.eth') - const stored = JSON.parse((await streamToBuffer(await content.asStream())).toString()) - - expect(stored).toMatchObject({ entityId, acl: Authenticator.signPayload(ownerIdentity.authChain, payload) }) Sinon.assert.calledWithMatch(metrics.increment, 'world_deployments_counter') }) @@ -172,9 +87,7 @@ test('deployment with failed validation', function ({ components, stubComponents // Sign entity id const identity = await getIdentity() - permissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'just-do-it.dcl.eth') - .resolves(false) + permissionChecker.validate.resolves(false) const authChain = Authenticator.signPayload(identity.authChain, entityId) @@ -183,11 +96,7 @@ test('deployment with failed validation', function ({ components, stubComponents 'Your wallet has no permission to publish this scene because it does not have permission to deploy under "just-do-it.dcl.eth". Check scene.json to select a name that either you own or you were given permission to deploy.' ) - Sinon.assert.calledWith( - permissionChecker.checkPermission, - identity.authChain.authChain[0].payload, - 'just-do-it.dcl.eth' - ) + Sinon.assert.calledOnce(permissionChecker.validate) expect(await storage.exist(fileHash)).toEqual(false) expect(await storage.exist(entityId)).toEqual(false) @@ -199,7 +108,7 @@ test('deployment with failed validation', function ({ components, stubComponents test('deployment with failed validation', function ({ components, stubComponents }) { it('does not work because user did not specify any names', async () => { const { config, storage } = components - const { permissionChecker, metrics } = stubComponents + const { metrics } = stubComponents const contentClient = new ContentClient({ contentUrl: `http://${await config.requireString('HTTP_SERVER_HOST')}:${await config.requireNumber( @@ -224,10 +133,6 @@ test('deployment with failed validation', function ({ components, stubComponents // Sign entity id const identity = await getIdentity() - permissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-super-name.dcl.eth') - .resolves(false) - const authChain = Authenticator.signPayload(identity.authChain, entityId) // Deploy entity @@ -235,8 +140,6 @@ test('deployment with failed validation', function ({ components, stubComponents 'Deployment failed: scene.json needs to specify a worldConfiguration section with a valid name inside.' ) - Sinon.assert.notCalled(permissionChecker.checkPermission) - expect(await storage.exist(fileHash)).toEqual(false) expect(await storage.exist(entityId)).toEqual(false) diff --git a/test/mocks/http-provider-mock.ts b/test/mocks/http-provider-mock.ts deleted file mode 100644 index d8f65029..00000000 --- a/test/mocks/http-provider-mock.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Callback, HTTPProvider, RPCMessage } from 'eth-connect' - -export function createHttpProviderMock(message?: any): HTTPProvider { - return { - host: '', - options: {}, - debug: false, - send: () => {}, - sendAsync: async (_payload: RPCMessage | RPCMessage[], _callback: Callback): Promise => { - _callback(null, message || {}) - } - } -} diff --git a/test/mocks/marketplace-subgraph-mock.ts b/test/mocks/marketplace-subgraph-mock.ts deleted file mode 100644 index 9a1367a7..00000000 --- a/test/mocks/marketplace-subgraph-mock.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ISubgraphComponent } from '@well-known-components/thegraph-component' - -export function createMockMarketplaceSubGraph(): ISubgraphComponent { - return { - query(): Promise { - return Promise.resolve({ - names: [] - } as T) - } - } -} diff --git a/test/mocks/world-name-permission-checker-mock.ts b/test/mocks/world-name-permission-checker-mock.ts index e9bc9a51..d32c224c 100644 --- a/test/mocks/world-name-permission-checker-mock.ts +++ b/test/mocks/world-name-permission-checker-mock.ts @@ -13,7 +13,11 @@ export function createMockNamePermissionChecker(names?: string[]): IWorldNamePer return { checkPermission, validate(deployment: DeploymentToValidate): Promise { - return Promise.resolve(false) + const sceneJson = JSON.parse(deployment.files.get(deployment.entity.id)!.toString()) + const worldSpecifiedName = sceneJson.metadata.worldConfiguration.name + return Promise.resolve( + names && names.map((name) => name.toLowerCase()).includes(worldSpecifiedName.toLowerCase()) + ) } } } diff --git a/test/unit/dcl-name-checker.spec.ts b/test/unit/dcl-name-checker.spec.ts index 6308634d..01692829 100644 --- a/test/unit/dcl-name-checker.spec.ts +++ b/test/unit/dcl-name-checker.spec.ts @@ -20,7 +20,6 @@ import { IDclNameChecker } from '../../src/types' describe('strategy builder', function () { let config: IConfigComponent let fetch: IFetchComponent - let logs: ILoggerComponent let metrics: IMetricsComponent beforeEach(async () => { diff --git a/test/unit/world-name-permission-checker.spec.ts b/test/unit/world-name-permission-checker.spec.ts index 7d7f1f49..fc8ceb2c 100644 --- a/test/unit/world-name-permission-checker.spec.ts +++ b/test/unit/world-name-permission-checker.spec.ts @@ -2,7 +2,8 @@ import { createConfigComponent } from '@well-known-components/env-config-provide import { createDclNamePlusACLPermissionChecker, createEndpointNameChecker, - createNoOpNameChecker + createNoOpNameChecker, + createWorldNamePermissionChecker } from '../../src/adapters/world-name-permission-checker' import { createLogComponent } from '@well-known-components/logger' import { IConfigComponent, ILoggerComponent } from '@well-known-components/interfaces' @@ -16,6 +17,95 @@ import { createInMemoryStorage } from '@dcl/catalyst-storage' import { IContentStorageComponent } from '@dcl/catalyst-storage/dist/types' import { Authenticator } from '@dcl/crypto' +describe('strategy builder', function () { + let config: IConfigComponent + let fetch: IFetchComponent + + beforeEach(async () => { + fetch = { + fetch: async (_url: Request): Promise => + new Response( + JSON.stringify({ + data: { nfts: [] } + }) + ) + } + }) + + it.each(['THE_GRAPH_DCL_NAME_CHECKER', 'ON_CHAIN_DCL_NAME_CHECKER'])( + 'it can build a DclNamePlusACLPermissionChecker', + async (nameValidator) => { + await expect( + createWorldNamePermissionChecker({ + dclNameChecker: undefined, + worldsManager: undefined, + config: createConfigComponent({ + LOG_LEVEL: 'DEBUG', + NAME_VALIDATOR: nameValidator, + MARKETPLACE_SUBGRAPH_URL: 'https://subgraph.com' + }), + fetch, + logs: await createLogComponent({ + config + }) + }) + ).resolves.toBeDefined() + } + ) + + it('it can build an EndpointNameChecker', async () => { + await expect( + createWorldNamePermissionChecker({ + dclNameChecker: undefined, + worldsManager: undefined, + config: createConfigComponent({ + LOG_LEVEL: 'DEBUG', + NAME_VALIDATOR: 'ENDPOINT_NAME_CHECKER', + ENDPOINT_NAME_CHECKER_BASE_URL: 'http://anything' + }), + fetch, + logs: await createLogComponent({ + config + }) + }) + ).resolves.toBeDefined() + }) + + it('it can build an NoOpNameChecker', async () => { + await expect( + createWorldNamePermissionChecker({ + dclNameChecker: undefined, + worldsManager: undefined, + config: createConfigComponent({ + LOG_LEVEL: 'DEBUG', + NAME_VALIDATOR: 'NOOP_NAME_CHECKER' + }), + fetch, + logs: await createLogComponent({ + config + }) + }) + ).resolves.toBeDefined() + }) + + it('it can build an NoOpNameChecker', async () => { + await expect( + createWorldNamePermissionChecker({ + dclNameChecker: undefined, + worldsManager: undefined, + config: createConfigComponent({ + LOG_LEVEL: 'DEBUG', + NAME_VALIDATOR: 'OTHER' + }), + fetch, + logs: await createLogComponent({ + config + }) + }) + ).rejects.toThrowError('Invalid nameValidatorStrategy selected: OTHER') + }) +}) + describe('dcl name checker + ACL', function () { let logs: ILoggerComponent let storage: IContentStorageComponent @@ -36,7 +126,6 @@ describe('dcl name checker + ACL', function () { describe('checkPermission', () => { it('when dcl name checker says yes, returns true', async () => { const permissionChecker = await createDclNamePlusACLPermissionChecker({ - logs, worldsManager, dclNameChecker: { checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { @@ -50,7 +139,6 @@ describe('dcl name checker + ACL', function () { it('when dcl name checker says no and no ACL configured returns false', async () => { const permissionChecker = await createDclNamePlusACLPermissionChecker({ - logs, worldsManager, dclNameChecker: { checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { @@ -73,7 +161,6 @@ describe('dcl name checker + ACL', function () { }) const permissionChecker = await createDclNamePlusACLPermissionChecker({ - logs, worldsManager, dclNameChecker: { checkOwnership(ethAddress: EthAddress, _worldName: string): Promise { @@ -95,7 +182,6 @@ describe('dcl name checker + ACL', function () { describe('validate', () => { it.each([false, true])('returns same value as checkPermission', async (expected) => { const permissionChecker = await createDclNamePlusACLPermissionChecker({ - logs, worldsManager, dclNameChecker: { checkOwnership(_ethAddress: EthAddress, _worldName: string): Promise { @@ -112,58 +198,89 @@ describe('dcl name checker + ACL', function () { }) }) -// describe('name checker: endpoint', function () { -// let logs: ILoggerComponent -// let config: IConfigComponent -// let fetch: IFetchComponent -// -// beforeEach(async () => { -// config = createConfigComponent({ -// LOG_LEVEL: 'DEBUG', -// ENDPOINT_NAME_CHECKER_BASE_URL: 'http://anything' -// }) -// logs = await createLogComponent({ config }) -// fetch = { -// fetch: async (_url: Request): Promise => new Response(undefined) -// } -// }) -// -// it('when permission asked for invalid name returns false', async () => { -// const permissionChecker = await createEndpointNameChecker({ -// config, -// fetch, -// logs -// }) -// -// await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() -// }) -// -// it('when permission asked for invalid address returns false', async () => { -// const permissionChecker = await createEndpointNameChecker({ -// config, -// fetch, -// logs -// }) -// -// await expect(permissionChecker.checkPermission('', 'anything')).resolves.toBeFalsy() -// }) -// -// it.each([true, false])('when valid name and address it returns as per the endpoint', async (value) => { -// fetch = { -// fetch: async (_url: Request): Promise => new Response(String(value)) -// } -// -// const permissionChecker = await createEndpointNameChecker({ -// config, -// fetch, -// logs -// }) -// -// const identity = await getIdentity() -// const address = identity.authChain.authChain[0].payload -// await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBe(value) -// }) -// }) +describe('name checker: endpoint', function () { + let config: IConfigComponent + let fetch: IFetchComponent + let identity + + beforeEach(async () => { + config = createConfigComponent({ + LOG_LEVEL: 'DEBUG', + ENDPOINT_NAME_CHECKER_BASE_URL: 'http://anything' + }) + fetch = { + fetch: async (_url: Request): Promise => new Response(undefined) + } + identity = await getIdentity() + }) + + describe('checkPermission', () => { + it('when permission asked for invalid name returns false', async () => { + const permissionChecker = await createEndpointNameChecker({ + config, + fetch + }) + + await expect(permissionChecker.checkPermission('0xb', '')).resolves.toBeFalsy() + }) + + it('when permission asked for invalid address returns false', async () => { + const permissionChecker = await createEndpointNameChecker({ + config, + fetch + }) + + await expect(permissionChecker.checkPermission('', 'anything')).resolves.toBeFalsy() + }) + + it.each([true, false])('when valid name and address it returns as per the endpoint', async (value) => { + fetch = { + fetch: async (_url: Request): Promise => new Response(String(value)) + } + + const permissionChecker = await createEndpointNameChecker({ + config, + fetch + }) + + const identity = await getIdentity() + const address = identity.authChain.authChain[0].payload + await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBe(value) + }) + }) + + describe('validate', () => { + it('when validate asked for invalid name returns false', async () => { + const permissionChecker = await createEndpointNameChecker({ + config, + fetch + }) + const deployment = await createDeployment(identity.authChain, { + type: EntityType.SCENE, + pointers: ['0,0'], + timestamp: Date.parse('2022-11-01T00:00:00Z'), + metadata: { worldConfiguration: { name: '' } }, + files: [] + }) + + await expect(permissionChecker.validate(deployment)).resolves.toBeFalsy() + }) + + it.each([true, false])('when valid name and address validate returns as per the endpoint', async (value) => { + fetch = { + fetch: async (_url: Request): Promise => new Response(String(value)) + } + + const permissionChecker = await createEndpointNameChecker({ + config, + fetch + }) + + const deployment = await createDeployment(identity.authChain) + await expect(permissionChecker.validate(deployment)).resolves.toBe(value) + }) + }) +}) describe('name checker: noop', () => { let permissionChecker: IWorldNamePermissionChecker @@ -184,7 +301,6 @@ describe('name checker: noop', () => { }) it('when valid name and address it returns true', async () => { - const identity = await getIdentity() const address = identity.authChain.authChain[0].payload await expect(permissionChecker.checkPermission(address, 'my-super-name.dcl.eth')).resolves.toBeTruthy() }) From 8d33e0e40a12f54380aa36bce28e1f5ff9874405 Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Fri, 3 Mar 2023 15:32:24 -0500 Subject: [PATCH 18/19] fix merge conflicts --- test/integration/acl-handlers.spec.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/test/integration/acl-handlers.spec.ts b/test/integration/acl-handlers.spec.ts index 26edf25a..308cd58f 100644 --- a/test/integration/acl-handlers.spec.ts +++ b/test/integration/acl-handlers.spec.ts @@ -319,14 +319,12 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when missing timestamp', async () => { const { localFetch } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() const delegatedIdentity = await getIdentity() - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const payload = `{"resource":"my-world.dcl.eth","allowed":["${delegatedIdentity.realAccount.address}"]}` @@ -347,14 +345,12 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when timestamp is too old', async () => { const { localFetch } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() const delegatedIdentity = await getIdentity() - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const ts = new Date(Date.now() - 500_000).toISOString() const payload = `{"resource":"my-world.dcl.eth","allowed":["${delegatedIdentity.realAccount.address}"],"timestamp":"${ts}"}` @@ -376,14 +372,12 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when timestamp is too far in the future', async () => { const { localFetch } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() const delegatedIdentity = await getIdentity() - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const ts = new Date(Date.now() + 500_000).toISOString() const payload = `{"resource":"my-world.dcl.eth","allowed":["${delegatedIdentity.realAccount.address}"],"timestamp":"${ts}"}` @@ -405,7 +399,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents test('acl handler POST /acl/:world_name', function ({ components, stubComponents }) { it('fails when new timestamp is after currently stored ACL', async () => { const { localFetch, storage } = components - const { namePermissionChecker } = stubComponents + const { dclNameChecker } = stubComponents const identity = await getIdentity() const delegatedIdentity = await getIdentity() @@ -418,9 +412,7 @@ test('acl handler POST /acl/:world_name', function ({ components, stubComponents acl: Authenticator.signPayload(identity.authChain, payload) }) - namePermissionChecker.checkPermission - .withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth') - .resolves(true) + dclNameChecker.checkOwnership.withArgs(identity.authChain.authChain[0].payload, 'my-world.dcl.eth').resolves(true) const newTs = new Date(Date.parse(ts) - 1).toISOString() const newPayload = `{"resource":"my-world.dcl.eth","allowed":["${delegatedIdentity.realAccount.address}"],"timestamp":"${newTs}"}` From a8589d77713a5771dc3e81a5b12d6a586045837a Mon Sep 17 00:00:00 2001 From: Mariano Goldman Date: Fri, 3 Mar 2023 15:36:47 -0500 Subject: [PATCH 19/19] cr updates --- src/components.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components.ts b/src/components.ts index a93705e3..42fcfe74 100644 --- a/src/components.ts +++ b/src/components.ts @@ -68,7 +68,7 @@ export async function initComponents(): Promise { const worldsManager = await createWorldsManagerComponent({ logs, storage }) - const namePermissionChecker = await createWorldNamePermissionChecker({ + const permissionChecker = await createWorldNamePermissionChecker({ config, dclNameChecker, fetch, @@ -78,7 +78,7 @@ export async function initComponents(): Promise { const validator = createValidator({ config, - permissionChecker: namePermissionChecker, + permissionChecker, limitsManager, storage, worldsManager @@ -88,7 +88,7 @@ export async function initComponents(): Promise { dclNameChecker, commsAdapter, config, - permissionChecker: namePermissionChecker, + permissionChecker, logs, server, statusChecks,