Merge pull request #1950 from Budibase/feature/user-session

Feature/user session
This commit is contained in:
Martin McKeaveney 2021-07-08 16:05:51 +01:00 committed by GitHub
commit 25b4c04d9b
24 changed files with 311 additions and 71 deletions

3
packages/auth/cache.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
user: require("./src/cache/user"),
}

View File

@ -0,0 +1 @@
module.exports = require("./src/security/sessions")

21
packages/auth/src/cache/user.js vendored Normal file
View File

@ -0,0 +1,21 @@
const { getDB } = require("../db")
const { StaticDatabases } = require("../db/utils")
const redis = require("../redis/authRedis")
const EXPIRY_SECONDS = 3600
exports.getUser = async userId => {
const client = await redis.getUserClient()
// try cache
let user = await client.get(userId)
if (!user) {
user = await getDB(StaticDatabases.GLOBAL.name).get(userId)
client.store(userId, user, EXPIRY_SECONDS)
}
return user
}
exports.invalidateUser = async userId => {
const client = await redis.getUserClient()
await client.delete(userId)
}

View File

@ -4,6 +4,7 @@ const JwtStrategy = require("passport-jwt").Strategy
const { StaticDatabases } = require("./db/utils") const { StaticDatabases } = require("./db/utils")
const { jwt, local, authenticated, google, auditLog } = require("./middleware") const { jwt, local, authenticated, google, auditLog } = require("./middleware")
const { setDB, getDB } = require("./db") const { setDB, getDB } = require("./db")
const userCache = require("./cache/user")
// Strategies // Strategies
passport.use(new LocalStrategy(local.options, local.authenticate)) passport.use(new LocalStrategy(local.options, local.authenticate))
@ -47,6 +48,9 @@ module.exports = {
jwt: require("jsonwebtoken"), jwt: require("jsonwebtoken"),
auditLog, auditLog,
}, },
cache: {
user: userCache,
},
StaticDatabases, StaticDatabases,
constants: require("./constants"), constants: require("./constants"),
} }

View File

@ -1,7 +1,7 @@
const { Cookies } = require("../constants") const { Cookies } = require("../constants")
const database = require("../db")
const { getCookie, clearCookie } = require("../utils") const { getCookie, clearCookie } = require("../utils")
const { StaticDatabases } = require("../db/utils") const { getUser } = require("../cache/user")
const { getSession, updateSessionTTL } = require("../security/sessions")
const env = require("../environment") const env = require("../environment")
const PARAM_REGEX = /\/:(.*?)\//g const PARAM_REGEX = /\/:(.*?)\//g
@ -48,14 +48,27 @@ module.exports = (noAuthPatterns = [], opts) => {
user = null, user = null,
internal = false internal = false
if (authCookie) { if (authCookie) {
try { let error = null
const db = database.getDB(StaticDatabases.GLOBAL.name) const sessionId = authCookie.sessionId,
user = await db.get(authCookie.userId) userId = authCookie.userId
delete user.password const session = await getSession(userId, sessionId)
authenticated = true if (!session) {
} catch (err) { error = "No session found"
// remove the cookie as the use does not exist anymore } else {
try {
user = await getUser(userId)
delete user.password
authenticated = true
} catch (err) {
error = err
}
}
if (error) {
// remove the cookie as the user does not exist anymore
clearCookie(ctx, Cookies.Auth) clearCookie(ctx, Cookies.Auth)
} else {
// make sure we denote that the session is still in use
await updateSessionTTL(session)
} }
} }
const apiKey = ctx.request.headers["x-budibase-api-key"] const apiKey = ctx.request.headers["x-budibase-api-key"]

View File

@ -7,6 +7,8 @@ const {
generateGlobalUserID, generateGlobalUserID,
ViewNames, ViewNames,
} = require("../../db/utils") } = require("../../db/utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
async function authenticate(token, tokenSecret, profile, done) { async function authenticate(token, tokenSecret, profile, done) {
// Check the user exists in the instance DB by email // Check the user exists in the instance DB by email
@ -59,15 +61,16 @@ async function authenticate(token, tokenSecret, profile, done) {
} }
// authenticate // authenticate
const payload = { const sessionId = newid()
userId: dbUser._id, await createASession(dbUser._id, sessionId)
builder: dbUser.builder,
email: dbUser.email,
}
dbUser.token = jwt.sign(payload, env.JWT_SECRET, { dbUser.token = jwt.sign(
expiresIn: "1 day", {
}) userId: dbUser._id,
sessionId,
},
env.JWT_SECRET
)
return done(null, dbUser) return done(null, dbUser)
} }

View File

@ -3,6 +3,8 @@ const { UserStatus } = require("../../constants")
const { compare } = require("../../hashing") const { compare } = require("../../hashing")
const env = require("../../environment") const env = require("../../environment")
const { getGlobalUserByEmail } = require("../../utils") const { getGlobalUserByEmail } = require("../../utils")
const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const INVALID_ERR = "Invalid Credentials" const INVALID_ERR = "Invalid Credentials"
@ -31,13 +33,16 @@ exports.authenticate = async function (email, password, done) {
// authenticate // authenticate
if (await compare(password, dbUser.password)) { if (await compare(password, dbUser.password)) {
const payload = { const sessionId = newid()
userId: dbUser._id, await createASession(dbUser._id, sessionId)
}
dbUser.token = jwt.sign(payload, env.JWT_SECRET, { dbUser.token = jwt.sign(
expiresIn: "1 day", {
}) userId: dbUser._id,
sessionId,
},
env.JWT_SECRET
)
// Remove users password in payload // Remove users password in payload
delete dbUser.password delete dbUser.password

View File

@ -0,0 +1,29 @@
const Client = require("./index")
const utils = require("./utils")
let userClient, sessionClient
async function init() {
userClient = await new Client(utils.Databases.USER_CACHE).init()
sessionClient = await new Client(utils.Databases.SESSIONS).init()
}
process.on("exit", async () => {
if (userClient) await userClient.finish()
if (sessionClient) await sessionClient.finish()
})
module.exports = {
getUserClient: async () => {
if (!userClient) {
await init()
}
return userClient
},
getSessionClient: async () => {
if (!sessionClient) {
await init()
}
return sessionClient
},
}

View File

@ -1,7 +1,12 @@
const env = require("../environment") const env = require("../environment")
// ioredis mock is all in memory // ioredis mock is all in memory
const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis")
const { addDbPrefix, removeDbPrefix, getRedisOptions } = require("./utils") const {
addDbPrefix,
removeDbPrefix,
getRedisOptions,
SEPARATOR,
} = require("./utils")
const RETRY_PERIOD_MS = 2000 const RETRY_PERIOD_MS = 2000
const STARTUP_TIMEOUT_MS = 5000 const STARTUP_TIMEOUT_MS = 5000
@ -143,14 +148,15 @@ class RedisWrapper {
CLIENT.disconnect() CLIENT.disconnect()
} }
async scan() { async scan(key = "") {
const db = this._db const db = this._db
key = `${db}${SEPARATOR}${key}`
let stream let stream
if (CLUSTERED) { if (CLUSTERED) {
let node = CLIENT.nodes("master") let node = CLIENT.nodes("master")
stream = node[0].scanStream({ match: db + "-*", count: 100 }) stream = node[0].scanStream({ match: key + "*", count: 100 })
} else { } else {
stream = CLIENT.scanStream({ match: db + "-*", count: 100 }) stream = CLIENT.scanStream({ match: key + "*", count: 100 })
} }
return promisifyStream(stream) return promisifyStream(stream)
} }
@ -182,6 +188,12 @@ class RedisWrapper {
} }
} }
async setExpiry(key, expirySeconds) {
const db = this._db
const prefixedKey = addDbPrefix(db, key)
await CLIENT.expire(prefixedKey, expirySeconds)
}
async delete(key) { async delete(key) {
const db = this._db const db = this._db
await CLIENT.del(addDbPrefix(db, key)) await CLIENT.del(addDbPrefix(db, key))

View File

@ -11,8 +11,12 @@ exports.Databases = {
INVITATIONS: "invitation", INVITATIONS: "invitation",
DEV_LOCKS: "devLocks", DEV_LOCKS: "devLocks",
DEBOUNCE: "debounce", DEBOUNCE: "debounce",
SESSIONS: "session",
USER_CACHE: "users",
} }
exports.SEPARATOR = SEPARATOR
exports.getRedisOptions = (clustered = false) => { exports.getRedisOptions = (clustered = false) => {
const [host, port] = REDIS_URL.split(":") const [host, port] = REDIS_URL.split(":")
const opts = { const opts = {

View File

@ -0,0 +1,69 @@
const redis = require("../redis/authRedis")
const EXPIRY_SECONDS = 86400
async function getSessionsForUser(userId) {
const client = await redis.getSessionClient()
const sessions = await client.scan(userId)
return sessions.map(session => session.value)
}
function makeSessionID(userId, sessionId) {
return `${userId}/${sessionId}`
}
exports.createASession = async (userId, sessionId) => {
const client = await redis.getSessionClient()
const session = {
createdAt: new Date().toISOString(),
lastAccessedAt: new Date().toISOString(),
sessionId,
userId,
}
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
}
exports.invalidateSessions = async (userId, sessionId = null) => {
let sessions = []
if (sessionId) {
sessions.push({ key: makeSessionID(userId, sessionId) })
} else {
sessions = await getSessionsForUser(userId)
}
const client = await redis.getSessionClient()
const promises = []
for (let session of sessions) {
promises.push(client.delete(session.key))
}
await Promise.all(promises)
}
exports.updateSessionTTL = async session => {
const client = await redis.getSessionClient()
const key = makeSessionID(session.userId, session.sessionId)
session.lastAccessedAt = new Date().toISOString()
await client.store(key, session, EXPIRY_SECONDS)
}
exports.endSession = async (userId, sessionId) => {
const client = await redis.getSessionClient()
await client.delete(makeSessionID(userId, sessionId))
}
exports.getUserSessions = getSessionsForUser
exports.getSession = async (userId, sessionId) => {
try {
const client = await redis.getSessionClient()
return client.get(makeSessionID(userId, sessionId))
} catch (err) {
// if can't get session don't error, just don't return anything
return null
}
}
exports.getAllSessions = async () => {
const client = await redis.getSessionClient()
const sessions = await client.scan()
return sessions.map(session => session.value)
}

View File

@ -64,23 +64,18 @@ exports.getCookie = (ctx, name) => {
} }
/** /**
* Store a cookie for the request, has a hardcoded expiry. * Store a cookie for the request - it will not expire.
* @param {object} ctx The request which is to be manipulated. * @param {object} ctx The request which is to be manipulated.
* @param {string} name The name of the cookie to set. * @param {string} name The name of the cookie to set.
* @param {string|object} value The value of cookie which will be set. * @param {string|object} value The value of cookie which will be set.
*/ */
exports.setCookie = (ctx, value, name = "builder") => { exports.setCookie = (ctx, value, name = "builder") => {
const expires = new Date()
expires.setDate(expires.getDate() + 1)
if (!value) { if (!value) {
ctx.cookies.set(name) ctx.cookies.set(name)
} else { } else {
value = jwt.sign(value, options.secretOrKey, { value = jwt.sign(value, options.secretOrKey)
expiresIn: "1 day",
})
ctx.cookies.set(name, value, { ctx.cookies.set(name, value, {
expires, maxAge: Number.MAX_SAFE_INTEGER,
path: "/", path: "/",
httpOnly: false, httpOnly: false,
overwrite: true, overwrite: true,

View File

@ -2,7 +2,6 @@ const setup = require("./utilities")
const { basicScreen } = setup.structures const { basicScreen } = setup.structures
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const workerRequests = require("../../../utilities/workerRequests")
const route = "/test" const route = "/test"

View File

@ -2,10 +2,10 @@ const { getAppId, setCookie, getCookie, clearCookie } =
require("@budibase/auth").utils require("@budibase/auth").utils
const { Cookies } = require("@budibase/auth").constants const { Cookies } = require("@budibase/auth").constants
const { getRole } = require("@budibase/auth/roles") const { getRole } = require("@budibase/auth/roles")
const { getGlobalSelf } = require("../utilities/workerRequests")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { generateUserMetadataID } = require("../db/utils") const { generateUserMetadataID } = require("../db/utils")
const { dbExists } = require("@budibase/auth/db") const { dbExists } = require("@budibase/auth/db")
const { getCachedSelf } = require("../utilities/global")
const CouchDB = require("../db") const CouchDB = require("../db")
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
@ -26,29 +26,17 @@ module.exports = async (ctx, next) => {
} }
} }
let updateCookie = false, let appId,
appId,
roleId = BUILTIN_ROLE_IDS.PUBLIC roleId = BUILTIN_ROLE_IDS.PUBLIC
if (!ctx.user) { if (!ctx.user) {
// not logged in, try to set a cookie for public apps // not logged in, try to set a cookie for public apps
updateCookie = true
appId = requestAppId appId = requestAppId
} else if ( } else if (requestAppId != null) {
requestAppId != null &&
(appCookie == null ||
requestAppId !== appCookie.appId ||
appCookie.roleId === BUILTIN_ROLE_IDS.PUBLIC ||
!appCookie.roleId)
) {
// Different App ID means cookie needs reset, or if the same public user has logged in // Different App ID means cookie needs reset, or if the same public user has logged in
const globalUser = await getGlobalSelf(ctx, requestAppId) const globalUser = await getCachedSelf(ctx, requestAppId)
updateCookie = true
appId = requestAppId appId = requestAppId
// retrieving global user gets the right role // retrieving global user gets the right role
roleId = globalUser.roleId || BUILTIN_ROLE_IDS.BASIC roleId = globalUser.roleId || BUILTIN_ROLE_IDS.BASIC
} else if (appCookie != null) {
appId = appCookie.appId
roleId = appCookie.roleId || BUILTIN_ROLE_IDS.BASIC
} }
// nothing more to do // nothing more to do
if (!appId) { if (!appId) {
@ -68,8 +56,12 @@ module.exports = async (ctx, next) => {
role: await getRole(appId, roleId), role: await getRole(appId, roleId),
} }
} }
if (updateCookie) { if (
setCookie(ctx, { appId, roleId }, Cookies.CurrentApp) requestAppId !== appId ||
appCookie == null ||
appCookie.appId !== requestAppId
) {
setCookie(ctx, { appId }, Cookies.CurrentApp)
} }
return next() return next()
} }

View File

@ -23,6 +23,15 @@ function mockReset() {
function mockAuthWithNoCookie() { function mockAuthWithNoCookie() {
jest.resetModules() jest.resetModules()
mockWorker() mockWorker()
jest.mock("@budibase/auth/cache", () => ({
user: {
getUser: () => {
return {
_id: "us_uuid1",
}
},
},
}))
jest.mock("@budibase/auth", () => ({ jest.mock("@budibase/auth", () => ({
utils: { utils: {
getAppId: jest.fn(), getAppId: jest.fn(),

View File

@ -17,6 +17,8 @@ const { cleanup } = require("../../utilities/fileSystem")
const { Cookies } = require("@budibase/auth").constants const { Cookies } = require("@budibase/auth").constants
const { jwt } = require("@budibase/auth").auth const { jwt } = require("@budibase/auth").auth
const { StaticDatabases } = require("@budibase/auth/db") const { StaticDatabases } = require("@budibase/auth/db")
const { createASession } = require("@budibase/auth/sessions")
const { user: userCache } = require("@budibase/auth/cache")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const GLOBAL_USER_ID = "us_uuid1" const GLOBAL_USER_ID = "us_uuid1"
@ -62,7 +64,7 @@ class TestConfiguration {
return request.body return request.body
} }
async globalUser(id = GLOBAL_USER_ID, builder = true) { async globalUser(id = GLOBAL_USER_ID, builder = true, roles) {
const db = new CouchDB(StaticDatabases.GLOBAL.name) const db = new CouchDB(StaticDatabases.GLOBAL.name)
let existing let existing
try { try {
@ -73,8 +75,9 @@ class TestConfiguration {
const user = { const user = {
_id: id, _id: id,
...existing, ...existing,
roles: {}, roles: roles || {},
} }
await createASession(id, "sessionid")
if (builder) { if (builder) {
user.builder = { global: true } user.builder = { global: true }
} }
@ -103,6 +106,7 @@ class TestConfiguration {
defaultHeaders() { defaultHeaders() {
const auth = { const auth = {
userId: GLOBAL_USER_ID, userId: GLOBAL_USER_ID,
sessionId: "sessionid",
} }
const app = { const app = {
roleId: BUILTIN_ROLE_IDS.ADMIN, roleId: BUILTIN_ROLE_IDS.ADMIN,
@ -138,13 +142,7 @@ class TestConfiguration {
roleId = BUILTIN_ROLE_IDS.ADMIN, roleId = BUILTIN_ROLE_IDS.ADMIN,
builder = false, builder = false,
}) { }) {
let user return this.login(email, PASSWORD, { roleId, builder })
try {
user = await this.createUser(email, PASSWORD, roleId)
} catch (err) {
// allow errors here
}
return this.login(email, PASSWORD, { roleId, userId: user._id, builder })
} }
async createApp(appName) { async createApp(appName) {
@ -313,6 +311,7 @@ class TestConfiguration {
async createUser(id = null) { async createUser(id = null) {
const globalId = !id ? `us_${Math.random()}` : `us_${id}` const globalId = !id ? `us_${Math.random()}` : `us_${id}`
const resp = await this.globalUser(globalId) const resp = await this.globalUser(globalId)
await userCache.invalidateUser(globalId)
return { return {
...resp, ...resp,
globalId, globalId,
@ -326,14 +325,19 @@ class TestConfiguration {
} }
// make sure the user exists in the global DB // make sure the user exists in the global DB
if (roleId !== BUILTIN_ROLE_IDS.PUBLIC) { if (roleId !== BUILTIN_ROLE_IDS.PUBLIC) {
await this.globalUser(userId, builder) const appId = `app${this.getAppId().split("app_dev")[1]}`
await this.globalUser(userId, builder, {
[appId]: roleId,
})
} }
if (!email || !password) { if (!email || !password) {
await this.createUser() await this.createUser()
} }
await createASession(userId, "sessionid")
// have to fake this // have to fake this
const auth = { const auth = {
userId, userId,
sessionId: "sessionid",
} }
const app = { const app = {
roleId: roleId, roleId: roleId,
@ -343,6 +347,7 @@ class TestConfiguration {
const appToken = jwt.sign(app, env.JWT_SECRET) const appToken = jwt.sign(app, env.JWT_SECRET)
// returning necessary request headers // returning necessary request headers
await userCache.invalidateUser(userId)
return { return {
Accept: "application/json", Accept: "application/json",
Cookie: [ Cookie: [

View File

@ -7,6 +7,7 @@ const {
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { getDeployedAppID } = require("@budibase/auth/db") const { getDeployedAppID } = require("@budibase/auth/db")
const { getGlobalUserParams } = require("@budibase/auth/db") const { getGlobalUserParams } = require("@budibase/auth/db")
const { user: userCache } = require("@budibase/auth/cache")
exports.updateAppRole = (appId, user) => { exports.updateAppRole = (appId, user) => {
if (!user.roles) { if (!user.roles) {
@ -25,15 +26,24 @@ exports.updateAppRole = (appId, user) => {
return user return user
} }
exports.getGlobalUser = async (appId, userId) => { function processUser(appId, user) {
const db = CouchDB(StaticDatabases.GLOBAL.name)
let user = await db.get(getGlobalIDFromUserMetadataID(userId))
if (user) { if (user) {
delete user.password delete user.password
} }
return exports.updateAppRole(appId, user) return exports.updateAppRole(appId, user)
} }
exports.getCachedSelf = async (ctx, appId) => {
const user = await userCache.getUser(ctx.user._id)
return processUser(appId, user)
}
exports.getGlobalUser = async (appId, userId) => {
const db = CouchDB(StaticDatabases.GLOBAL.name)
let user = await db.get(getGlobalIDFromUserMetadataID(userId))
return processUser(appId, user)
}
exports.getGlobalUsers = async (appId = null, users = null) => { exports.getGlobalUsers = async (appId = null, users = null) => {
const db = CouchDB(StaticDatabases.GLOBAL.name) const db = CouchDB(StaticDatabases.GLOBAL.name)
let globalUsers let globalUsers

View File

@ -10,7 +10,7 @@ const { checkResetPasswordCode } = require("../../../utilities/redis")
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
function authInternal(ctx, user, err = null) { async function authInternal(ctx, user, err = null) {
if (err) { if (err) {
return ctx.throw(403, "Unauthorized") return ctx.throw(403, "Unauthorized")
} }
@ -22,6 +22,7 @@ function authInternal(ctx, user, err = null) {
return ctx.throw(403, "Unauthorized") return ctx.throw(403, "Unauthorized")
} }
// just store the user ID
ctx.cookies.set(Cookies.Auth, user.token, { ctx.cookies.set(Cookies.Auth, user.token, {
expires, expires,
path: "/", path: "/",
@ -32,7 +33,7 @@ function authInternal(ctx, user, err = null) {
exports.authenticate = async (ctx, next) => { exports.authenticate = async (ctx, next) => {
return passport.authenticate("local", async (err, user) => { return passport.authenticate("local", async (err, user) => {
authInternal(ctx, user, err) await authInternal(ctx, user, err)
delete user.token delete user.token
@ -123,7 +124,7 @@ exports.googleAuth = async (ctx, next) => {
strategy, strategy,
{ successRedirect: "/", failureRedirect: "/error" }, { successRedirect: "/", failureRedirect: "/error" },
async (err, user) => { async (err, user) => {
authInternal(ctx, user, err) await authInternal(ctx, user, err)
ctx.redirect("/") ctx.redirect("/")
} }

View File

@ -0,0 +1,37 @@
const {
getAllSessions,
getUserSessions,
invalidateSessions,
} = require("@budibase/auth/sessions")
exports.fetch = async ctx => {
ctx.body = await getAllSessions()
}
exports.find = async ctx => {
const { userId } = ctx.params
const sessions = await getUserSessions(userId)
ctx.body = sessions.map(session => session.value)
}
exports.invalidateUser = async ctx => {
const { userId } = ctx.params
await invalidateSessions(userId)
ctx.body = {
message: "User sessions invalidated",
}
}
exports.selfSessions = async ctx => {
const userId = ctx.user._id
ctx.body = await getUserSessions(userId)
}
exports.invalidateSession = async ctx => {
const userId = ctx.user._id
const { sessionId } = ctx.params
await invalidateSessions(userId, sessionId)
ctx.body = {
message: "Session invalidated successfully.",
}
}

View File

@ -5,6 +5,8 @@ const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils
const { UserStatus, EmailTemplatePurpose } = require("../../../constants") const { UserStatus, EmailTemplatePurpose } = require("../../../constants")
const { checkInviteCode } = require("../../../utilities/redis") const { checkInviteCode } = require("../../../utilities/redis")
const { sendEmail } = require("../../../utilities/email") const { sendEmail } = require("../../../utilities/email")
const { user: userCache } = require("@budibase/auth/cache")
const { invalidateSessions } = require("@budibase/auth/sessions")
const GLOBAL_DB = StaticDatabases.GLOBAL.name const GLOBAL_DB = StaticDatabases.GLOBAL.name
@ -62,6 +64,7 @@ exports.save = async ctx => {
password: hashedPassword, password: hashedPassword,
...user, ...user,
}) })
await userCache.invalidateUser(response.id)
ctx.body = { ctx.body = {
_id: response.id, _id: response.id,
_rev: response.rev, _rev: response.rev,
@ -107,6 +110,8 @@ exports.destroy = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const dbUser = await db.get(ctx.params.id) const dbUser = await db.get(ctx.params.id)
await db.remove(dbUser._id, dbUser._rev) await db.remove(dbUser._id, dbUser._rev)
await userCache.invalidateUser(dbUser._id)
await invalidateSessions(dbUser._id)
ctx.body = { ctx.body = {
message: `User ${ctx.params.id} deleted.`, message: `User ${ctx.params.id} deleted.`,
} }
@ -117,13 +122,16 @@ exports.removeAppRole = async ctx => {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const users = await allUsers() const users = await allUsers()
const bulk = [] const bulk = []
const cacheInvalidations = []
for (let user of users) { for (let user of users) {
if (user.roles[appId]) { if (user.roles[appId]) {
cacheInvalidations.push(userCache.invalidateUser(user._id))
delete user.roles[appId] delete user.roles[appId]
bulk.push(user) bulk.push(user)
} }
} }
await db.bulkDocs(bulk) await db.bulkDocs(bulk)
await Promise.all(cacheInvalidations)
ctx.body = { ctx.body = {
message: "App role removed from all users", message: "App role removed from all users",
} }
@ -153,6 +161,7 @@ exports.updateSelf = async ctx => {
...user, ...user,
...ctx.request.body, ...ctx.request.body,
}) })
await userCache.invalidateUser(user._id)
ctx.body = { ctx.body = {
_id: response.id, _id: response.id,
_rev: response.rev, _rev: response.rev,

View File

@ -0,0 +1,14 @@
const Router = require("@koa/router")
const controller = require("../../controllers/admin/sessions")
const adminOnly = require("../../../middleware/adminOnly")
const router = Router()
router
.get("/api/admin/sessions", adminOnly, controller.fetch)
.get("/api/admin/sessions/self", controller.selfSessions)
.get("/api/admin/sessions/:userId", adminOnly, controller.find)
.delete("/api/admin/sessions/:userId", adminOnly, controller.invalidateUser)
.delete("/api/admin/sessions/self/:sessionId", controller.invalidateSession)
module.exports = router

View File

@ -5,6 +5,7 @@ const templateRoutes = require("./admin/templates")
const emailRoutes = require("./admin/email") const emailRoutes = require("./admin/email")
const authRoutes = require("./admin/auth") const authRoutes = require("./admin/auth")
const roleRoutes = require("./admin/roles") const roleRoutes = require("./admin/roles")
const sessionRoutes = require("./admin/sessions")
const appRoutes = require("./app") const appRoutes = require("./app")
exports.routes = [ exports.routes = [
@ -15,5 +16,6 @@ exports.routes = [
appRoutes, appRoutes,
templateRoutes, templateRoutes,
emailRoutes, emailRoutes,
sessionRoutes,
roleRoutes, roleRoutes,
] ]

View File

@ -5,6 +5,7 @@ const { jwt } = require("@budibase/auth").auth
const { Cookies } = require("@budibase/auth").constants const { Cookies } = require("@budibase/auth").constants
const { Configs, LOGO_URL } = require("../../../../constants") const { Configs, LOGO_URL } = require("../../../../constants")
const { getGlobalUserByEmail } = require("@budibase/auth").utils const { getGlobalUserByEmail } = require("@budibase/auth").utils
const { createASession } = require("@budibase/auth/sessions")
class TestConfiguration { class TestConfiguration {
constructor(openServer = true) { constructor(openServer = true) {
@ -56,6 +57,7 @@ class TestConfiguration {
null, null,
controllers.users.save controllers.users.save
) )
await createASession("us_uuid1", "sessionid")
} }
} }
@ -69,6 +71,7 @@ class TestConfiguration {
const user = { const user = {
_id: "us_uuid1", _id: "us_uuid1",
userId: "us_uuid1", userId: "us_uuid1",
sessionId: "sessionid",
} }
const authToken = jwt.sign(user, env.JWT_SECRET) const authToken = jwt.sign(user, env.JWT_SECRET)
return { return {

View File

@ -44,7 +44,7 @@ async function getACode(db, code, deleteCode = true) {
exports.init = async () => { exports.init = async () => {
pwResetClient = await new Client(utils.Databases.PW_RESETS).init() pwResetClient = await new Client(utils.Databases.PW_RESETS).init()
invitationClient = await new Client(utils.Databases.PW_RESETS).init() invitationClient = await new Client(utils.Databases.INVITATIONS).init()
} }
/** /**