packages/fxa-auth-server/lib/redis.js (183 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ const { RedisShared } = require('fxa-shared/db/redis'); const { resolve } = require('path'); const { AuthLogger } = require('./types'); const { Container } = require('typedi'); const { StatsD } = require('hot-shots'); const opentelemetry = require('@opentelemetry/api'); ('use strict'); const tracer = opentelemetry.trace.getTracer('redis-tracer'); const hex = require('buf').to.hex; function resolveLogger() { if (Container.has(AuthLogger)) return Container.get(AuthLogger); } function resolveMetrics() { if (Container.has(StatsD)) { return Container.get(StatsD); } } class FxaRedis extends RedisShared { constructor(config) { super(config, resolveLogger(), resolveMetrics()); // Applies custom scripts which are turned into methods on // the redis object. const scriptsDirectory = resolve(__dirname, 'luaScripts'); this.defineCommands(this.redis, scriptsDirectory); } /** * * @param {AccessToken} token */ async setAccessToken(token) { if (token.ttl < 1) { this.log.error('redis', new Error('invalid ttl on access token')); return; } this.metrics?.increment('redis.setAccessToken'); const span = tracer.startSpan('redis.setAccessToken'); const value = JSON.stringify(token); const result = await this.redis.setAccessToken( token.userId.toString('hex'), token.tokenId.toString('hex'), value, this.recordLimit, token.ttl, this.maxttl ); span.end(); return result; } /** * * @param {Buffer | string} id * @returns {Promise<boolean>} done */ async removeAccessToken(id) { this.metrics?.increment('redis.removeAccessToken'); const span = tracer.startSpan('redis.removeAccessToken'); // This does not remove the id from the user's index // because getAccessTokens cleans up expired/missing tokens const done = await this.redis.removeAccessToken(hex(id)); span.end(); return !!done; } /** * * @param {Buffer | string} uid */ async removeAccessTokensForPublicClients(uid) { this.metrics?.increment('redis.removeAccessTokensForPublicClients'); const span = tracer.startSpan('redis.removeAccessTokensForPublicClients'); const result = await this.redis.removeAccessTokensForPublicClients( hex(uid) ); span.end(); return result; } /** * * @param {Buffer | string} uid * @param {Buffer | string} clientId */ async removeAccessTokensForUserAndClient(uid, clientId) { this.metrics?.increment('redis.removeAccessTokensForUserAndClient'); const span = tracer.startSpan('redis.removeAccessTokensForUserAndClient'); const result = await this.redis.removeAccessTokensForUserAndClient( hex(uid), hex(clientId) ); span.end(); return result; } /** * * @param {Buffer | string} uid */ async removeAccessTokensForUser(uid) { this.metrics?.increment('redis.removeAccessTokensForUser'); const span = tracer.startSpan('redis.removeAccessTokensForUser'); const result = await this.redis.removeAccessTokensForUser(hex(uid)); span.end(); return result; } /** * @param {Buffer | string} uid * @param {Buffer | string} tokenId * @param {RefreshTokenMetadata} token */ async setRefreshToken(uid, tokenId, token) { this.metrics?.increment('redis.setRefreshToken'); const span = tracer.startSpan('redis.setRefreshToken'); const p1 = this.redis.setRefreshToken( hex(uid), hex(tokenId), JSON.stringify(token), this.recordLimit, this.maxttl ); const p2 = this.resolveInMs(p1, this.timeoutMs); const result = await Promise.race([p1, p2]); span.end(); return result; } /** * * @param {Buffer | string} uid * @param {Buffer | string} tokenId */ async removeRefreshToken(uid, tokenId) { this.metrics?.increment('redis.removeRefreshToken'); const span = await tracer.startSpan('redis.removeRefreshToken'); const p1 = this.redis.hdel(hex(uid), hex(tokenId)); const p2 = this.resolveInMs(p1, this.timeoutMs); const result = await Promise.race([p1, p2]); span.end(); return result; } /** * * @param {Buffer | string} uid */ async removeRefreshTokensForUser(uid) { this.metrics?.increment('redis.removeRefreshTokensForUser'); const span = tracer.startSpan('redis.removeRefreshTokensForUser'); const p1 = this.redis.unlink(hex(uid)); const p2 = this.resolveInMs(p1, this.timeoutMs); const result = await Promise.race([p1, p2]); span.end(); return result; } async get(key) { this.metrics?.increment('redis.get'); const span = tracer.startSpan('redis.get'); const result = await this.redis.get(key); if (result?.length > 0) { span.setAttribute('redis.get.size', result.length); this.metrics?.histogram('redis.get.size', result.length); } span.end(); return result; } set(key, val, ...args) { return this.redis.set(key, val, ...args); } zadd(key, ...args) { return this.redis.zadd(key, ...args); } zrange(key, start, stop, withScores) { if (withScores) { return this.redis.zrange(key, start, stop, 'WITHSCORES'); } return this.redis.zrange(key, start, stop); } zrangebyscore(key, min, max) { return this.redis.zrangebyscore(key, min, max); } zrem(key, ...members) { return this.redis.zrem(key, members); } zrevrange(key, start, stop) { return this.redis.zrevrange(key, start, stop); } zrevrangebyscore(key, min, max) { return this.redis.zrevrangebyscore(key, min, max); } zrank(key, member) { return this.redis.zrank(key, member); } keys(key) { return this.redis.keys(key); } async zpoprangebyscore(key, min, max) { const args = Array.from(arguments); const results = await this.redis .multi() .zrangebyscore(...args) .zremrangebyscore(key, min, max) .exec(); return results[0][1]; } } module.exports = (config, log) => { log = log || resolveLogger(); if (!config) { log?.warn('redis', { msg: `No redis config provided`, stack: Error().stack, }); return; } if (!config.enabled) { log?.warn('redis', { msg: `Redis not enabled, config.enabled:${config?.enabled} `, stack: Error().stack, }); return; } // Sanity check if (!config.host || !config.port) { log?.warn('redis', { msg: `No redis host/port defined, config.host:${config?.host} config.port:${config?.port}`, stack: Error().stack, }); } return new FxaRedis(config, log); }; // module.exports.FxaRedis = FxaRedis;