From 1ccde1a796b16d81cc64945de5dcad80a0703360 Mon Sep 17 00:00:00 2001 From: Julien Genestoux Date: Fri, 26 Apr 2024 11:47:45 -0400 Subject: [PATCH] feat(locksmith): Event protected attributes (#13677) * checking for protected attributes on events * feat(locksmith): adding protected attributes for events * getLock now can include protected keys * making sure we protect notifyCheckInUrls --- .../operations/eventOperations.test.ts | 7 ++- .../controllers/v2/customEmailController.ts | 2 +- .../src/controllers/v2/eventsController.ts | 36 +++++++++++++- .../src/controllers/v2/ticketsController.tsx | 6 ++- locksmith/src/operations/eventOperations.ts | 20 ++++++-- .../src/operations/lockSettingOperations.ts | 11 +++-- .../src/operations/metadataOperations.ts | 6 ++- .../middlewares/eventOrganizerMiddleware.ts | 5 +- .../utils/middlewares/isVerifierMiddleware.ts | 48 +++++++++++-------- locksmith/src/utils/protectedAttributes.ts | 16 +++++++ 10 files changed, 121 insertions(+), 36 deletions(-) create mode 100644 locksmith/src/utils/protectedAttributes.ts diff --git a/locksmith/__tests__/operations/eventOperations.test.ts b/locksmith/__tests__/operations/eventOperations.test.ts index a298ee2e9ba..00e0d1df261 100644 --- a/locksmith/__tests__/operations/eventOperations.test.ts +++ b/locksmith/__tests__/operations/eventOperations.test.ts @@ -71,7 +71,7 @@ describe('eventOperations', () => { checkoutConfig: { config: { locks: {} } }, } const [{ slug }] = await saveEvent(eventParams, '0x123') - const savedEvent = await getEventBySlug(slug) + const savedEvent = await getEventBySlug(slug, false) expect(savedEvent.slug).toEqual('my-rsvp-party') expect(savedEvent?.data.requiresApproval).toEqual(true) }) @@ -136,7 +136,7 @@ describe('eventOperations', () => { }, } const [{ slug }] = await saveEvent(eventParams, '0x123') - const savedEvent = await getEventBySlug(slug) + const savedEvent = await getEventBySlug(slug, true) expect(savedEvent.slug).toEqual('an-event-with-all-the-data') expect(savedEvent?.data).toEqual({ name: 'An Event with all the data', @@ -166,6 +166,9 @@ describe('eventOperations', () => { emailSender: 'Julien Genestoux', requiresApproval: false, }) + + const savedEventWithoutProtectedData = await getEventBySlug(slug, false) + expect(savedEventWithoutProtectedData?.data.replyTo).toEqual(undefined) }) }) }) diff --git a/locksmith/src/controllers/v2/customEmailController.ts b/locksmith/src/controllers/v2/customEmailController.ts index 0120bb2c551..84d52d9098f 100644 --- a/locksmith/src/controllers/v2/customEmailController.ts +++ b/locksmith/src/controllers/v2/customEmailController.ts @@ -119,7 +119,7 @@ export const EventInviteBody = z.object({ export const sendEventInvite: RequestHandler = async (request, response) => { const slug = request.params.slug.toLowerCase().trim() - const event = await getEventBySlug(slug) + const event = await getEventBySlug(slug, true) const { recipients } = await EventInviteBody.parseAsync(request.body) const results = await Promise.all( diff --git a/locksmith/src/controllers/v2/eventsController.ts b/locksmith/src/controllers/v2/eventsController.ts index 0d7c305adea..9f7482d1737 100644 --- a/locksmith/src/controllers/v2/eventsController.ts +++ b/locksmith/src/controllers/v2/eventsController.ts @@ -11,6 +11,8 @@ import { getLockSettingsBySlug } from '../../operations/lockSettingOperations' import { getLockMetadata } from '../../operations/metadataOperations' import { PaywallConfig, PaywallConfigType } from '@unlock-protocol/core' import listManagers from '../../utils/lockManagers' +import { removeProtectedAttributesFromObject } from '../../utils/protectedAttributes' +import { isVerifierOrManagerForLock } from '../../utils/middlewares/isVerifierMiddleware' // DEPRECATED! export const getEventDetailsByLock: RequestHandler = async ( @@ -65,6 +67,9 @@ export const getAllEvents: RequestHandler = async (request, response) => { offset: (page - 1) * 10, include: [{ model: CheckoutConfig, as: 'checkoutConfig' }], }) + events.forEach((event) => { + event.data = removeProtectedAttributesFromObject(event.data) + }) return response.status(200).send({ data: events, page, @@ -76,7 +81,10 @@ export const getAllEvents: RequestHandler = async (request, response) => { // whose slug matches and get the event data from that lock. export const getEvent: RequestHandler = async (request, response) => { const slug = request.params.slug.toLowerCase().trim() - const event = await getEventBySlug(slug) + const event = await getEventBySlug( + slug, + true /** includeProtected and we will cleanup later */ + ) if (event) { const eventResponse = event.toJSON() as any // TODO: type! @@ -88,6 +96,32 @@ export const getEvent: RequestHandler = async (request, response) => { }, }) } + + // Check if the caller is a verifier or manager and remove protected attributes if not + let isManagerOrVerifier = false + + if (request.user) { + const locks = Object.keys(eventResponse.checkoutConfig.config.locks) + for (let i = 0; i < locks.length; i++) { + if (!isManagerOrVerifier) { + const lock = locks[i] + const network = + eventResponse.checkoutConfig.config.locks[lock].network || + eventResponse.checkoutConfig.config.network + isManagerOrVerifier = await isVerifierOrManagerForLock( + lock, + request.user.walletAddress, + network + ) + } + } + } + if (!isManagerOrVerifier) { + eventResponse.data = removeProtectedAttributesFromObject( + eventResponse.data + ) + } + return response.status(200).send(eventResponse) } diff --git a/locksmith/src/controllers/v2/ticketsController.tsx b/locksmith/src/controllers/v2/ticketsController.tsx index ba9ff08d776..5306da621c2 100644 --- a/locksmith/src/controllers/v2/ticketsController.tsx +++ b/locksmith/src/controllers/v2/ticketsController.tsx @@ -109,7 +109,11 @@ export class TicketsController { } ) - const event = await getEventForLock(lockAddress, network) + const event = await getEventForLock( + lockAddress, + network, + true /** includeProtected */ + ) const web3Service = new Web3Service(networks) const tokenOwner = await web3Service.ownerOf(lockAddress, id, network) diff --git a/locksmith/src/operations/eventOperations.ts b/locksmith/src/operations/eventOperations.ts index 670b5d326fe..eb966eb43f4 100644 --- a/locksmith/src/operations/eventOperations.ts +++ b/locksmith/src/operations/eventOperations.ts @@ -6,6 +6,7 @@ import { CheckoutConfig, EventData } from '../models' import { saveCheckoutConfig } from './checkoutConfigOperations' import { EventBodyType } from '../controllers/v2/eventsController' import { Op } from 'sequelize' +import { removeProtectedAttributesFromObject } from '../utils/protectedAttributes' interface AttributeProps { value: string @@ -39,7 +40,8 @@ const getEventDate = ( export const getEventForLock = async ( lockAddress: string, - network?: number + network: number, + includeProtected: boolean ) => { const checkoutConfigs = await CheckoutConfig.findAll({ where: { @@ -58,6 +60,9 @@ export const getEventForLock = async ( checkoutConfigId: checkoutConfigs.map((record) => record.id), }, }) + if (event && !includeProtected) { + event.data = removeProtectedAttributesFromObject(event.data) + } return event } @@ -155,12 +160,19 @@ export const getEventMetadataForLock = async ( return eventDetail } -export const getEventBySlug = async (slug: string) => { - return await EventData.findOne({ +export const getEventBySlug = async ( + slug: string, + includeProtected: boolean +) => { + const event = await EventData.findOne({ where: { slug, }, }) + if (event && !includeProtected) { + event.data = removeProtectedAttributesFromObject(event.data) + } + return event } export const createEventSlug = async ( @@ -171,7 +183,7 @@ export const createEventSlug = async ( const slug = index ? kebabCase([cleanName, index].join('-')) : kebabCase(cleanName) - const event = await getEventBySlug(slug) + const event = await getEventBySlug(slug, false /** includeProtected */) if (event) { return createEventSlug(name, index ? index + 1 : 1) } diff --git a/locksmith/src/operations/lockSettingOperations.ts b/locksmith/src/operations/lockSettingOperations.ts index 6a08755ce20..9de624b4baf 100644 --- a/locksmith/src/operations/lockSettingOperations.ts +++ b/locksmith/src/operations/lockSettingOperations.ts @@ -4,6 +4,7 @@ import { } from '../controllers/v2/lockSettingController' import { LockSetting } from '../models/lockSetting' import * as Normalizer from '../utils/normalizer' +import { protectedAttributes } from '../utils/protectedAttributes' import { getEventForLock } from './eventOperations' interface SendEmailProps { @@ -39,9 +40,7 @@ export async function getSettings({ includeProtected?: boolean }): Promise { // list of array of keys to exclude - const attributesExcludes = includeProtected - ? [] - : ['replyTo', 'promoCodes', 'passwords'] + const attributesExcludes = includeProtected ? [] : protectedAttributes const settings = await LockSetting.findOne({ where: { @@ -55,7 +54,11 @@ export async function getSettings({ const lockSettings = settings || { ...DEFAULT_LOCK_SETTINGS } - const eventDetails = await getEventForLock(lockAddress, network) + const eventDetails = await getEventForLock( + lockAddress, + network, + includeProtected + ) if (eventDetails?.data) { if (eventDetails?.data.replyTo) { lockSettings.replyTo = eventDetails.data.replyTo diff --git a/locksmith/src/operations/metadataOperations.ts b/locksmith/src/operations/metadataOperations.ts index 4a1ce557eb9..96268026568 100644 --- a/locksmith/src/operations/metadataOperations.ts +++ b/locksmith/src/operations/metadataOperations.ts @@ -290,7 +290,11 @@ export const getLockMetadata = async ({ } // Now let's see if there is an event data that needs to be attached to this lock! - const event = await getEventForLock(lockAddress, network) + const event = await getEventForLock( + lockAddress, + network, + false /** includeProtected, metadata is always public */ + ) // Add the event data! if (event) { diff --git a/locksmith/src/utils/middlewares/eventOrganizerMiddleware.ts b/locksmith/src/utils/middlewares/eventOrganizerMiddleware.ts index 4f5893d1e4f..0953801aeb3 100644 --- a/locksmith/src/utils/middlewares/eventOrganizerMiddleware.ts +++ b/locksmith/src/utils/middlewares/eventOrganizerMiddleware.ts @@ -27,7 +27,10 @@ export const eventOrganizerMiddleware: RequestHandler = async ( // If this is an existing event! if (slug) { - const existingEvent = await getEventBySlug(slug) + const existingEvent = await getEventBySlug( + slug, + false /** includeProtected */ + ) if (existingEvent?.checkoutConfigId) { const checkoutConfig = await getCheckoutConfigById( existingEvent.checkoutConfigId diff --git a/locksmith/src/utils/middlewares/isVerifierMiddleware.ts b/locksmith/src/utils/middlewares/isVerifierMiddleware.ts index ebe347015f7..7faced815f6 100644 --- a/locksmith/src/utils/middlewares/isVerifierMiddleware.ts +++ b/locksmith/src/utils/middlewares/isVerifierMiddleware.ts @@ -5,33 +5,39 @@ import Normalizer from '../normalizer' import { logger } from '@sentry/utils' import { Verifier } from '../../models/verifier' +export const isVerifierOrManagerForLock = async ( + lockAddress: string, + address: string, + network: number +) => { + let isLockManager = false + + const verifier = await Verifier.findOne({ + where: { + lockAddress, + address, + network, + }, + }) + + if (!verifier) { + const web3Service = new Web3Service(networks) + isLockManager = await web3Service.isLockManager( + lockAddress, + address, + network + ) + } + return verifier?.id !== undefined || isLockManager +} + export const isVerifierMiddleware: RequestHandler = async (req, res, next) => { try { const network = Number(req.params.network) const lockAddress = Normalizer.ethereumAddress(req.params.lockAddress) const address = Normalizer.ethereumAddress(req.user!.walletAddress!) - let isLockManager = false - - const isVerifier = await Verifier.findOne({ - where: { - lockAddress, - address, - network, - }, - }) - - if (!isVerifier) { - const web3Service = new Web3Service(networks) - isLockManager = await web3Service.isLockManager( - lockAddress, - address, - network - ) - } - const isVerifierOrManager = isVerifier?.id !== undefined || isLockManager - - if (isVerifierOrManager) { + if (await isVerifierOrManagerForLock(lockAddress, address, network)) { return next() } else { return res.status(403).send({ diff --git a/locksmith/src/utils/protectedAttributes.ts b/locksmith/src/utils/protectedAttributes.ts new file mode 100644 index 00000000000..96a4b69ae17 --- /dev/null +++ b/locksmith/src/utils/protectedAttributes.ts @@ -0,0 +1,16 @@ +export const protectedAttributes = [ + 'replyTo', + 'promoCodes', + 'passwords', + 'notifyCheckInUrls', +] + +export const removeProtectedAttributesFromObject = (obj: any) => { + const newObject = { + ...obj, + } + protectedAttributes.forEach((attr) => { + delete newObject[attr] + }) + return newObject +}