Merge pull request #1950 from Budibase/feature/user-session
Feature/user session
This commit is contained in:
commit
25b4c04d9b
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
user: require("./src/cache/user"),
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./src/security/sessions")
|
|
@ -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)
|
||||||
|
}
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
|
@ -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))
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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("/")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.",
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
@ -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,
|
||||||
]
|
]
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue