Bypass rate limits for admins and moderators
This commit is contained in:
parent
f823637d18
commit
e5a781ec25
6 changed files with 50 additions and 10 deletions
|
@ -1,6 +1,6 @@
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import RateLimit from 'express-rate-limit'
|
import { buildRateLimiter } from '@server/middlewares'
|
||||||
import { HttpStatusCode } from '../../../shared/models'
|
import { HttpStatusCode } from '../../../shared/models'
|
||||||
import { badRequest } from '../../helpers/express-utils'
|
import { badRequest } from '../../helpers/express-utils'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { CONFIG } from '../../initializers/config'
|
||||||
|
@ -29,7 +29,7 @@ apiRouter.use(cors({
|
||||||
credentials: true
|
credentials: true
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const apiRateLimiter = RateLimit({
|
const apiRateLimiter = buildRateLimiter({
|
||||||
windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
|
windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
|
||||||
max: CONFIG.RATES_LIMIT.API.MAX
|
max: CONFIG.RATES_LIMIT.API.MAX
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import RateLimit from 'express-rate-limit'
|
|
||||||
import { tokensRouter } from '@server/controllers/api/users/token'
|
import { tokensRouter } from '@server/controllers/api/users/token'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks'
|
import { Hooks } from '@server/lib/plugins/hooks'
|
||||||
import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
|
import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
|
||||||
|
@ -17,9 +16,11 @@ import { Notifier } from '../../../lib/notifier'
|
||||||
import { Redis } from '../../../lib/redis'
|
import { Redis } from '../../../lib/redis'
|
||||||
import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
|
import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
|
||||||
import {
|
import {
|
||||||
|
adminUsersSortValidator,
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
buildRateLimiter,
|
||||||
ensureUserHasRight,
|
ensureUserHasRight,
|
||||||
ensureUserRegistrationAllowed,
|
ensureUserRegistrationAllowed,
|
||||||
ensureUserRegistrationAllowedForIP,
|
ensureUserRegistrationAllowedForIP,
|
||||||
|
@ -32,7 +33,6 @@ import {
|
||||||
usersListValidator,
|
usersListValidator,
|
||||||
usersRegisterValidator,
|
usersRegisterValidator,
|
||||||
usersRemoveValidator,
|
usersRemoveValidator,
|
||||||
adminUsersSortValidator,
|
|
||||||
usersUpdateValidator
|
usersUpdateValidator
|
||||||
} from '../../../middlewares'
|
} from '../../../middlewares'
|
||||||
import {
|
import {
|
||||||
|
@ -54,13 +54,13 @@ import { myVideoPlaylistsRouter } from './my-video-playlists'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('users')
|
const auditLogger = auditLoggerFactory('users')
|
||||||
|
|
||||||
const signupRateLimiter = RateLimit({
|
const signupRateLimiter = buildRateLimiter({
|
||||||
windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
|
windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
|
||||||
max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
|
max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
|
||||||
skipFailedRequests: true
|
skipFailedRequests: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const askSendEmailLimiter = RateLimit({
|
const askSendEmailLimiter = buildRateLimiter({
|
||||||
windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
|
windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
|
||||||
max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
|
max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import RateLimit from 'express-rate-limit'
|
|
||||||
import { logger } from '@server/helpers/logger'
|
import { logger } from '@server/helpers/logger'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
import { CONFIG } from '@server/initializers/config'
|
||||||
import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
|
import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
|
||||||
import { handleOAuthToken } from '@server/lib/auth/oauth'
|
import { handleOAuthToken } from '@server/lib/auth/oauth'
|
||||||
import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
|
import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks'
|
import { Hooks } from '@server/lib/plugins/hooks'
|
||||||
import { asyncMiddleware, authenticate, openapiOperationDoc } from '@server/middlewares'
|
import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares'
|
||||||
import { buildUUID } from '@shared/extra-utils'
|
import { buildUUID } from '@shared/extra-utils'
|
||||||
import { ScopedToken } from '@shared/models/users/user-scoped-token'
|
import { ScopedToken } from '@shared/models/users/user-scoped-token'
|
||||||
|
|
||||||
const tokensRouter = express.Router()
|
const tokensRouter = express.Router()
|
||||||
|
|
||||||
const loginRateLimiter = RateLimit({
|
const loginRateLimiter = buildRateLimiter({
|
||||||
windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
|
windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
|
||||||
max: CONFIG.RATES_LIMIT.LOGIN.MAX
|
max: CONFIG.RATES_LIMIT.LOGIN.MAX
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,6 +4,7 @@ export * from './activitypub'
|
||||||
export * from './async'
|
export * from './async'
|
||||||
export * from './auth'
|
export * from './auth'
|
||||||
export * from './pagination'
|
export * from './pagination'
|
||||||
|
export * from './rate-limiter'
|
||||||
export * from './robots'
|
export * from './robots'
|
||||||
export * from './servers'
|
export * from './servers'
|
||||||
export * from './sort'
|
export * from './sort'
|
||||||
|
|
31
server/middlewares/rate-limiter.ts
Normal file
31
server/middlewares/rate-limiter.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { UserRole } from '@shared/models'
|
||||||
|
import RateLimit from 'express-rate-limit'
|
||||||
|
import { optionalAuthenticate } from './auth'
|
||||||
|
|
||||||
|
const whitelistRoles = new Set([ UserRole.ADMINISTRATOR, UserRole.MODERATOR ])
|
||||||
|
|
||||||
|
function buildRateLimiter (options: {
|
||||||
|
windowMs: number
|
||||||
|
max: number
|
||||||
|
skipFailedRequests?: boolean
|
||||||
|
}) {
|
||||||
|
return RateLimit({
|
||||||
|
windowMs: options.windowMs,
|
||||||
|
max: options.max,
|
||||||
|
skipFailedRequests: options.skipFailedRequests,
|
||||||
|
|
||||||
|
handler: (req, res, next, options) => {
|
||||||
|
return optionalAuthenticate(req, res, () => {
|
||||||
|
if (res.locals.authenticated === true && whitelistRoles.has(res.locals.oauth.token.User.role)) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(options.statusCode).send(options.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildRateLimiter
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServ
|
||||||
|
|
||||||
describe('Test application behind a reverse proxy', function () {
|
describe('Test application behind a reverse proxy', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
let userAccessToken: string
|
||||||
let videoId: string
|
let videoId: string
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
@ -34,6 +35,8 @@ describe('Test application behind a reverse proxy', function () {
|
||||||
server = await createSingleServer(1, config)
|
server = await createSingleServer(1, config)
|
||||||
await setAccessTokensToServers([ server ])
|
await setAccessTokensToServers([ server ])
|
||||||
|
|
||||||
|
userAccessToken = await server.users.generateUserAndToken('user')
|
||||||
|
|
||||||
const { uuid } = await server.videos.upload()
|
const { uuid } = await server.videos.upload()
|
||||||
videoId = uuid
|
videoId = uuid
|
||||||
})
|
})
|
||||||
|
@ -93,7 +96,7 @@ describe('Test application behind a reverse proxy', function () {
|
||||||
it('Should rate limit logins', async function () {
|
it('Should rate limit logins', async function () {
|
||||||
const user = { username: 'root', password: 'fail' }
|
const user = { username: 'root', password: 'fail' }
|
||||||
|
|
||||||
for (let i = 0; i < 19; i++) {
|
for (let i = 0; i < 18; i++) {
|
||||||
await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +144,12 @@ describe('Test application behind a reverse proxy', function () {
|
||||||
await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
|
await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should rate limit API calls with a user but not with an admin', async function () {
|
||||||
|
await server.videos.get({ id: videoId, token: userAccessToken, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
|
||||||
|
|
||||||
|
await server.videos.get({ id: videoId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests([ server ])
|
await cleanupTests([ server ])
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue