Skip to content

Commit

Permalink
[PAY-604] Use /abuse endpoint in Identity (#3860)
Browse files Browse the repository at this point in the history
* Integrate AAO /abuse endpoint

* Integrate AAO

* Polish

* Skip abuse config

* Fix migration

* Fixes

* Nullable columns for faster migration

* polish
  • Loading branch information
piazzatron committed Sep 22, 2022
1 parent 0eff09d commit 56ab786
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict'

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.addColumn('Users', 'isBlockedFromRelay', {
type: Sequelize.BOOLEAN,
allowNull: true
}, { transaction })

await queryInterface.addColumn('Users', 'isBlockedFromNotifications', {
type: Sequelize.BOOLEAN,
allowNull: true
}, { transaction })

await queryInterface.addColumn('Users', 'appliedRules', {
type: Sequelize.JSONB,
allowNull: true
}, { transaction })

await queryInterface.sequelize.query(
'UPDATE "Users" SET "isBlockedFromRelay" = true WHERE "isAbusive" = true AND "isAbusiveErrorCode" != \'9\'',
{ transaction }
)

await queryInterface.sequelize.query(
'UPDATE "Users" SET "isBlockedFromNotifications" = true WHERE "isAbusive" = true AND "isAbusiveErrorCode" = \'9\'',
{ transaction }
)

// Safe to drop this now since we've moved users
await queryInterface.removeColumn('Users', 'isAbusive', { transaction })
await queryInterface.removeColumn('Users', 'isAbusiveErrorCode', { transaction })
})
},

down: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction(async (transaction) => {
await queryInterface.removeColumn('Users', 'isBlockedFromRelay', { transaction })
await queryInterface.removeColumn('Users', 'isBlockedFromNotifications', { transaction })
await queryInterface.removeColumn('Users', 'appliedRules', { transaction })
await queryInterface.addColumn(
'Users',
'isAbusive', {
type: Sequelize.BOOLEAN,
defaultValue: false
}
)
await queryInterface.addColumn(
'Users',
'isAbusiveErrorCode', {
type: Sequelize.STRING
}
)
})
}
}
16 changes: 9 additions & 7 deletions identity-service/src/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ module.exports = (sequelize, DataTypes) => {
allowNull: false,
defaultValue: false
},
isAbusive: {
isBlockedFromRelay: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
allowNull: true
},
isAbusiveErrorCode: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null
isBlockedFromNotifications: {
type: DataTypes.BOOLEAN,
allowNull: true
},
appliedRules: {
type: DataTypes.JSONB,
allowNull: true
},
isEmailDeliverable: {
type: DataTypes.BOOLEAN,
Expand Down
4 changes: 2 additions & 2 deletions identity-service/src/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,11 +459,11 @@ async function filterOutAbusiveUsers (notifications) {
where: {
blockchainUserId: { [ models.Sequelize.Op.in ]: allUserIds }
},
attributes: ['blockchainUserId', 'isAbusive']
attributes: ['blockchainUserId', 'isBlockedFromNotifications', 'isBlockedFromRelay']
})
const usersAbuseMap = {}
users.forEach(user => {
usersAbuseMap[user.blockchainUserId] = user.isAbusive
usersAbuseMap[user.blockchainUserId] = user.isBlockedFromRelay || user.isBlockedFromNotifications
})
const result = notifications.filter(notification => {
const isInitiatorAbusive = usersAbuseMap[notification.initiator.toString()]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ const publishNotifications = async (notifications, metadata, userNotificationSet

// Don't publish events for deactivated users
const isReceiverDeactivated = metadata.users[userId] && metadata.users[userId].is_deactivated
const isInitiatorAbusive = initiatorMap[initiatorUserId] && initiatorMap[initiatorUserId].isAbusive
const initiatingUser = initiatorMap[initiatorUserId]
const isInitiatorAbusive = initiatorMap[initiatorUserId] && (initiatingUser.isBlockedFromRelay || initiatingUser.isBlockedFromNotifications)
if (isReceiverDeactivated) {
continue
}
Expand Down
19 changes: 11 additions & 8 deletions identity-service/src/routes/relay.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const { getFeatureFlag, FEATURE_FLAGS } = require('../featureFlag')
const models = require('../models')
const { getIP } = require('../utils/antiAbuse')

const blockRelayAbuseErrorCodes = new Set(['0', '8', '9', '10'])

module.exports = function (app) {
// TODO(roneilr): authenticate that user controls senderAddress somehow, potentially validate that
// method sig has come from sender address as well
Expand All @@ -20,7 +18,7 @@ module.exports = function (app) {
// TODO: Use auth middleware to derive this
const user = await models.User.findOne({
where: { walletAddress: body.senderAddress },
attributes: ['id', 'blockchainUserId', 'walletAddress', 'handle', 'isAbusive', 'isAbusiveErrorCode']
attributes: ['id', 'blockchainUserId', 'walletAddress', 'handle', 'isBlockedFromRelay', 'isBlockedFromNotifications', 'appliedRules']
})

let optimizelyClient
Expand All @@ -34,21 +32,26 @@ module.exports = function (app) {
req.logger.error(`failed to retrieve optimizely feature flag for ${FEATURE_FLAGS.DETECT_ABUSE_ON_RELAY} or ${FEATURE_FLAGS.BLOCK_ABUSE_ON_RELAY}: ${error}`)
}

// Handle abusive users

const userFlaggedAsAbusive = user && (user.isBlockedFromRelay || user.isBlockedFromNotifications)
if (
blockAbuseOnRelay &&
user &&
user.isAbusiveErrorCode &&
blockRelayAbuseErrorCodes.has(user.isAbusiveErrorCode)
userFlaggedAsAbusive
) {
// allow previously abusive users to redeem themselves for next relays
if (detectAbuseOnRelay) {
const reqIP = req.get('X-Forwarded-For') || req.ip
detectAbuse(user, 'relay', reqIP) // fired & forgotten
}

return errorResponseForbidden(
`Forbidden ${user.isAbusiveErrorCode}`
)
// Only reject relay for users explicitly blocked from relay
if (user.isBlockedFromRelay) {
return errorResponseForbidden(
`Forbidden ${user.appliedRules}`
)
}
}

if (body && body.contractRegistryKey && body.contractAddress && body.senderAddress && body.encodedABI) {
Expand Down
48 changes: 25 additions & 23 deletions identity-service/src/utils/antiAbuse.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const models = require('../models')

const aaoEndpoint = config.get('aaoEndpoint') || 'https://antiabuseoracle.audius.co'

const blockRelayAbuseErrorCodes = new Set([0, 8, 10])
const blockNotificationsErrorCodes = new Set([9])

/**
* Gets IP of a user by using the leftmost forwarded-for entry
* or defaulting to req.ip
Expand Down Expand Up @@ -34,54 +37,53 @@ const recordIP = async (userIP, handle) => {
}
}

const getAbuseAttestation = async (challengeId, handle, reqIP) => {
const res = await axios.post(`${aaoEndpoint}/attestation/${handle}`, {
challengeId,
challengeSpecifier: handle,
amount: 0
}, {
const getAbuseData = async (handle, reqIP) => {
const res = await axios.get(`${aaoEndpoint}/abuse/${handle}`, {
headers: {
'X-Forwarded-For': reqIP
}
})

const data = res.data
logger.info(`antiAbuse: aao response: ${JSON.stringify(data)}`)
return data
const { data: rules } = res
const appliedRules = rules.filter(r => r.trigger && r.action === 'fail').map(r => r.rule)
const blockedFromRelay = appliedRules.some(r => blockRelayAbuseErrorCodes.has(r))
const blockedFromNotifications = appliedRules.some(r => blockNotificationsErrorCodes.has(r))
return { blockedFromRelay, blockedFromNotifications, appliedRules }
}

const detectAbuse = async (user, challengeId, reqIP) => {
let isAbusive = false
let isAbusiveErrorCode = null
const detectAbuse = async (user, reqIP) => {
let blockedFromRelay = false
let blockedFromNotifications = false
let appliedRules = null

if (!user.handle) {
// Something went wrong during sign up and identity has no knowledge
// of this user's handle. Flag them as abusive.
isAbusive = true
blockedFromRelay = true
} else {
try {
// Write out the latest user IP to Identity DB - AAO will request it back
await recordIP(reqIP, user.handle)

const { result, errorCode } = await getAbuseAttestation(challengeId, user.handle, reqIP)
if (!result) {
// The anti abuse system deems them abusive. Flag them as such.
isAbusive = true
if (errorCode || errorCode === 0) {
isAbusiveErrorCode = `${errorCode}`
}
// Perform abuse check conditional on environment
if (config.get('skipAbuseCheck')) {
logger.info(`Skipping abuse check for user ${user.handle}`)
} else {
;({ appliedRules, blockedFromRelay, blockedFromNotifications } = await getAbuseData(user.handle, reqIP))
}
} catch (e) {
logger.warn(`antiAbuse: aao request failed ${e.message}`)
}
}
if (user.isAbusive !== isAbusive || user.isAbusiveErrorCode !== isAbusiveErrorCode) {
await user.update({ isAbusive, isAbusiveErrorCode })

// Use !! for nullable columns :(
if (!!user.isBlockedFromRelay !== blockedFromRelay || !!user.isBlockedFromNotifications !== blockedFromNotifications) {
await user.update({ isBlockedFromRelay: blockedFromRelay, isBlockedFromNotifications: blockedFromNotifications, appliedRules })
}
}

module.exports = {
getAbuseAttestation,
getAbuseAttestation: getAbuseData,
detectAbuse,
getIP,
recordIP
Expand Down

0 comments on commit 56ab786

Please sign in to comment.