diff --git a/lerna.json b/lerna.json index c35a2dbdc9..78f7618651 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.70", + "version": "0.9.71", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/auth/cache.js b/packages/auth/cache.js new file mode 100644 index 0000000000..48563a16f3 --- /dev/null +++ b/packages/auth/cache.js @@ -0,0 +1,3 @@ +module.exports = { + user: require("./src/cache/user"), +} diff --git a/packages/auth/package.json b/packages/auth/package.json index 9155d37eeb..690d232459 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.70", + "version": "0.9.71", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/sessions.js b/packages/auth/sessions.js new file mode 100644 index 0000000000..c07efa2380 --- /dev/null +++ b/packages/auth/sessions.js @@ -0,0 +1 @@ +module.exports = require("./src/security/sessions") diff --git a/packages/auth/src/cache/user.js b/packages/auth/src/cache/user.js new file mode 100644 index 0000000000..46202cbfe9 --- /dev/null +++ b/packages/auth/src/cache/user.js @@ -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) +} diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index c56c5c5a05..98c558706a 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -11,6 +11,7 @@ const { auditLog, } = require("./middleware") const { setDB, getDB } = require("./db") +const userCache = require("./cache/user") // Strategies passport.use(new LocalStrategy(local.options, local.authenticate)) @@ -55,6 +56,9 @@ module.exports = { jwt: require("jsonwebtoken"), auditLog, }, + cache: { + user: userCache, + }, StaticDatabases, constants: require("./constants"), } diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index 64494f709d..db1fdfacd9 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -1,7 +1,7 @@ const { Cookies } = require("../constants") -const database = require("../db") 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 PARAM_REGEX = /\/:(.*?)\//g @@ -48,14 +48,27 @@ module.exports = (noAuthPatterns = [], opts) => { user = null, internal = false if (authCookie) { - try { - const db = database.getDB(StaticDatabases.GLOBAL.name) - user = await db.get(authCookie.userId) - delete user.password - authenticated = true - } catch (err) { - // remove the cookie as the use does not exist anymore + let error = null + const sessionId = authCookie.sessionId, + userId = authCookie.userId + const session = await getSession(userId, sessionId) + if (!session) { + error = "No session found" + } 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) + } else { + // make sure we denote that the session is still in use + await updateSessionTTL(session) } } const apiKey = ctx.request.headers["x-budibase-api-key"] diff --git a/packages/auth/src/middleware/passport/local.js b/packages/auth/src/middleware/passport/local.js index 01d1e00934..16b53bf894 100644 --- a/packages/auth/src/middleware/passport/local.js +++ b/packages/auth/src/middleware/passport/local.js @@ -4,6 +4,8 @@ const { compare } = require("../../hashing") const env = require("../../environment") const { getGlobalUserByEmail } = require("../../utils") const { authError } = require("./utils") +const { newid } = require("../../hashing") +const { createASession } = require("../../security/sessions") const INVALID_ERR = "Invalid Credentials" @@ -32,13 +34,16 @@ exports.authenticate = async function (email, password, done) { // authenticate if (await compare(password, dbUser.password)) { - const payload = { - userId: dbUser._id, - } + const sessionId = newid() + await createASession(dbUser._id, sessionId) - dbUser.token = jwt.sign(payload, env.JWT_SECRET, { - expiresIn: "1 day", - }) + dbUser.token = jwt.sign( + { + userId: dbUser._id, + sessionId, + }, + env.JWT_SECRET + ) // Remove users password in payload delete dbUser.password diff --git a/packages/auth/src/middleware/passport/oidc.js b/packages/auth/src/middleware/passport/oidc.js index 6b39a0b20e..a1e3039121 100644 --- a/packages/auth/src/middleware/passport/oidc.js +++ b/packages/auth/src/middleware/passport/oidc.js @@ -110,7 +110,7 @@ exports.strategyFactory = async function (config, callbackUrl) { userInfoURL: body.userinfo_endpoint, clientID: clientId, clientSecret: clientSecret, - callbackURL: callbackUrl + callbackURL: callbackUrl, }, authenticate ) diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index c11465ec3b..82083236ca 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -7,6 +7,8 @@ const { ViewNames, } = require("../../db/utils") const { authError } = require("./utils") +const { newid } = require("../../hashing") +const { createASession } = require("../../security/sessions") /** * Common authentication logic for third parties. e.g. OAuth, OIDC. @@ -57,7 +59,7 @@ exports.authenticateThirdParty = async function ( } // exit early if there is still no user and auto creation is disabled - if (!dbUser && requireLocalAccount ) { + if (!dbUser && requireLocalAccount) { if (requireLocalAccount) { return authError( done, @@ -82,15 +84,16 @@ exports.authenticateThirdParty = async function ( dbUser._rev = response.rev // authenticate - const payload = { - userId: dbUser._id, - builder: dbUser.builder, - email: dbUser.email, - } + const sessionId = newid() + await createASession(dbUser._id, sessionId) - dbUser.token = jwt.sign(payload, env.JWT_SECRET, { - expiresIn: "1 day", - }) + dbUser.token = jwt.sign( + { + userId: dbUser._id, + sessionId, + }, + env.JWT_SECRET + ) return done(null, dbUser) } @@ -120,7 +123,7 @@ function syncUser(user, thirdPartyUser) { user.lastName = name.familyName } } - + // profile // @reviewers: Historically stored at the root level of the user // Nest to prevent conflicts with future fields diff --git a/packages/auth/src/redis/authRedis.js b/packages/auth/src/redis/authRedis.js new file mode 100644 index 0000000000..decce6763b --- /dev/null +++ b/packages/auth/src/redis/authRedis.js @@ -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 + }, +} diff --git a/packages/auth/src/redis/index.js b/packages/auth/src/redis/index.js index e20255bfd3..4f2b5288ea 100644 --- a/packages/auth/src/redis/index.js +++ b/packages/auth/src/redis/index.js @@ -1,7 +1,12 @@ const env = require("../environment") // ioredis mock is all in memory 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 STARTUP_TIMEOUT_MS = 5000 @@ -143,14 +148,15 @@ class RedisWrapper { CLIENT.disconnect() } - async scan() { + async scan(key = "") { const db = this._db + key = `${db}${SEPARATOR}${key}` let stream if (CLUSTERED) { let node = CLIENT.nodes("master") - stream = node[0].scanStream({ match: db + "-*", count: 100 }) + stream = node[0].scanStream({ match: key + "*", count: 100 }) } else { - stream = CLIENT.scanStream({ match: db + "-*", count: 100 }) + stream = CLIENT.scanStream({ match: key + "*", count: 100 }) } 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) { const db = this._db await CLIENT.del(addDbPrefix(db, key)) diff --git a/packages/auth/src/redis/utils.js b/packages/auth/src/redis/utils.js index 23702353d8..415dcbf463 100644 --- a/packages/auth/src/redis/utils.js +++ b/packages/auth/src/redis/utils.js @@ -11,8 +11,12 @@ exports.Databases = { INVITATIONS: "invitation", DEV_LOCKS: "devLocks", DEBOUNCE: "debounce", + SESSIONS: "session", + USER_CACHE: "users", } +exports.SEPARATOR = SEPARATOR + exports.getRedisOptions = (clustered = false) => { const [host, port] = REDIS_URL.split(":") const opts = { diff --git a/packages/auth/src/security/roles.js b/packages/auth/src/security/roles.js index 53e1b90d73..baa8fc40dc 100644 --- a/packages/auth/src/security/roles.js +++ b/packages/auth/src/security/roles.js @@ -147,7 +147,7 @@ exports.getRole = async (appId, roleId) => { */ async function getAllUserRoles(appId, userRoleId) { if (!userRoleId) { - return [BUILTIN_IDS.PUBLIC] + return [BUILTIN_IDS.BASIC] } let currentRole = await exports.getRole(appId, userRoleId) let roles = currentRole ? [currentRole] : [] @@ -226,7 +226,7 @@ exports.getAllRoles = async appId => { dbRole => exports.getExternalRoleID(dbRole._id) === builtinRoleId )[0] if (dbBuiltin == null) { - roles.push(builtinRole) + roles.push(builtinRole || builtinRoles.BASIC) } else { // remove role and all back after combining with the builtin roles = roles.filter(role => role._id !== dbBuiltin._id) diff --git a/packages/auth/src/security/sessions.js b/packages/auth/src/security/sessions.js new file mode 100644 index 0000000000..4051df7123 --- /dev/null +++ b/packages/auth/src/security/sessions.js @@ -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) +} diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index 278ad07174..8bd635e2e3 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -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 {string} name The name of the cookie to set. * @param {string|object} value The value of cookie which will be set. */ exports.setCookie = (ctx, value, name = "builder") => { - const expires = new Date() - expires.setDate(expires.getDate() + 1) - if (!value) { ctx.cookies.set(name) } else { - value = jwt.sign(value, options.secretOrKey, { - expiresIn: "1 day", - }) + value = jwt.sign(value, options.secretOrKey) ctx.cookies.set(name, value, { - expires, + maxAge: Number.MAX_SAFE_INTEGER, path: "/", httpOnly: false, overwrite: true, diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 9b2b2823ef..a4b6349128 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "0.9.70", + "version": "0.9.71", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/bbui/src/ColorPicker/ColorPicker.svelte b/packages/bbui/src/ColorPicker/ColorPicker.svelte index 2f4b79b91a..4d248d6190 100644 --- a/packages/bbui/src/ColorPicker/ColorPicker.svelte +++ b/packages/bbui/src/ColorPicker/ColorPicker.svelte @@ -9,10 +9,10 @@ export let value export let size = "M" + export let spectrumTheme let open = false - $: color = value || "transparent" $: customValue = getCustomValue(value) $: checkColor = getCheckColor(value) @@ -21,7 +21,8 @@ { label: "Grays", colors: [ - "white", + "gray-50", + "gray-75", "gray-100", "gray-200", "gray-300", @@ -31,7 +32,6 @@ "gray-700", "gray-800", "gray-900", - "black", ], }, { @@ -86,7 +86,7 @@ return value } let found = false - const comparisonValue = value.substring(35, value.length - 1) + const comparisonValue = value.substring(28, value.length - 1) for (let category of categories) { found = category.colors.includes(comparisonValue) if (found) { @@ -102,17 +102,19 @@ const getCheckColor = value => { return /^.*(white|(gray-(50|75|100|200|300|400|500)))\)$/.test(value) - ? "black" - : "white" + ? "var(--spectrum-global-color-gray-900)" + : "var(--spectrum-global-color-gray-50)" }