Updating auth to utilise the tenant system.

This commit is contained in:
mike12345567 2021-07-16 15:08:58 +01:00
parent 912659a8ad
commit b7995dd61d
16 changed files with 61 additions and 50 deletions

View File

@ -6,7 +6,7 @@ const EXPIRY_SECONDS = 3600
exports.getUser = async (userId, tenantId = null) => { exports.getUser = async (userId, tenantId = null) => {
if (!tenantId) { if (!tenantId) {
tenantId = await lookupTenantId({ userId }) tenantId = await lookupTenantId(userId)
} }
const client = await redis.getUserClient() const client = await redis.getUserClient()
// try cache // try cache

View File

@ -4,6 +4,7 @@ const { getDB } = require("./index")
const UNICODE_MAX = "\ufff0" const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_" const SEPARATOR = "_"
const DEFAULT_TENANT = "default"
exports.ViewNames = { exports.ViewNames = {
USER_BY_EMAIL: "by_email", USER_BY_EMAIL: "by_email",
@ -72,19 +73,20 @@ function getDocParams(docType, docId = null, otherProps = {}) {
* Gets the name of the global DB to connect to in a multi-tenancy system. * Gets the name of the global DB to connect to in a multi-tenancy system.
*/ */
exports.getGlobalDB = tenantId => { exports.getGlobalDB = tenantId => {
const globalName = exports.StaticDatabases.GLOBAL.name
// fallback for system pre multi-tenancy // fallback for system pre multi-tenancy
if (!tenantId) { let dbName = exports.StaticDatabases.GLOBAL.name
return globalName if (tenantId && tenantId !== DEFAULT_TENANT) {
dbName = `${tenantId}${SEPARATOR}${dbName}`
} }
return getDB(`${tenantId}${SEPARATOR}${globalName}`) return getDB(dbName)
} }
/** /**
* Given a koa context this tries to find the correct tenant Global DB. * Given a koa context this tries to find the correct tenant Global DB.
*/ */
exports.getGlobalDBFromCtx = ctx => { exports.getGlobalDBFromCtx = ctx => {
return exports.getGlobalDB(ctx.user.tenantId) const user = ctx.user || {}
return exports.getGlobalDB(user.tenantId)
} }
/** /**

View File

@ -1,7 +1,7 @@
const passport = require("koa-passport") const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy const JwtStrategy = require("passport-jwt").Strategy
const { getGlobalDB } = require("./db/utils") const { getGlobalDB, StaticDatabases } = require("./db/utils")
const { jwt, local, authenticated, google, auditLog } = require("./middleware") const { jwt, local, authenticated, google, auditLog } = require("./middleware")
const { setDB } = require("./db") const { setDB } = require("./db")
const userCache = require("./cache/user") const userCache = require("./cache/user")

View File

@ -13,7 +13,7 @@ const { lookupTenantId } = require("../../utils")
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
const userId = generateGlobalUserID(profile.id) const userId = generateGlobalUserID(profile.id)
const tenantId = await lookupTenantId({ userId }) const tenantId = await lookupTenantId(userId)
const db = getGlobalDB(tenantId) const db = getGlobalDB(tenantId)
let dbUser let dbUser

View File

@ -8,20 +8,27 @@ const { createASession } = require("../../security/sessions")
const INVALID_ERR = "Invalid Credentials" const INVALID_ERR = "Invalid Credentials"
exports.options = {} exports.options = {
passReqToCallback: true,
}
/** /**
* Passport Local Authentication Middleware. * Passport Local Authentication Middleware.
* @param {*} email - username to login with * @param {*} ctx the request structure
* @param {*} password - plain text password to log in with * @param {*} email username to login with
* @param {*} done - callback from passport to return user information and errors * @param {*} password plain text password to log in with
* @param {*} done callback from passport to return user information and errors
* @returns The authenticated user, or errors if they occur * @returns The authenticated user, or errors if they occur
*/ */
exports.authenticate = async function (email, password, done) { exports.authenticate = async function (ctx, email, password, done) {
if (!email) return done(null, false, "Email Required.") if (!email) return done(null, false, "Email Required.")
if (!password) return done(null, false, "Password Required.") if (!password) return done(null, false, "Password Required.")
const params = ctx.params || {}
const query = ctx.query || {}
const dbUser = await getGlobalUserByEmail(email) // use the request to find the tenantId
const tenantId = params.tenantId || query.tenantId
const dbUser = await getGlobalUserByEmail(email, tenantId)
if (dbUser == null) { if (dbUser == null) {
return done(null, false, { message: "User not found" }) return done(null, false, { message: "User not found" })
} }

View File

@ -101,10 +101,9 @@ exports.isClient = ctx => {
return ctx.headers["x-budibase-type"] === "client" return ctx.headers["x-budibase-type"] === "client"
} }
exports.lookupTenantId = async ({ email, userId }) => { exports.lookupTenantId = async userId => {
const toQuery = email || userId
const db = getDB(StaticDatabases.PLATFORM_INFO.name) const db = getDB(StaticDatabases.PLATFORM_INFO.name)
const doc = await db.get(toQuery) const doc = await db.get(userId)
if (!doc || !doc.tenantId) { if (!doc || !doc.tenantId) {
throw "Unable to find tenant" throw "Unable to find tenant"
} }
@ -118,13 +117,10 @@ exports.lookupTenantId = async ({ email, userId }) => {
* @param {string|null} tenantId If tenant ID is known it can be specified * @param {string|null} tenantId If tenant ID is known it can be specified
* @return {Promise<object|null>} * @return {Promise<object|null>}
*/ */
exports.getGlobalUserByEmail = async (email, tenantId = null) => { exports.getGlobalUserByEmail = async (email, tenantId) => {
if (email == null) { if (email == null) {
throw "Must supply an email address to view" throw "Must supply an email address to view"
} }
if (!tenantId) {
tenantId = await exports.lookupTenantId({ email })
}
const db = getGlobalDB(tenantId) const db = getGlobalDB(tenantId)
try { try {
let users = ( let users = (
@ -138,7 +134,7 @@ exports.getGlobalUserByEmail = async (email, tenantId = null) => {
} catch (err) { } catch (err) {
if (err != null && err.name === "not_found") { if (err != null && err.name === "not_found") {
await createUserEmailView(db) await createUserEmailView(db)
return exports.getGlobalUserByEmail(email) return exports.getGlobalUserByEmail(email, tenantId)
} else { } else {
throw err throw err
} }

View File

@ -1,16 +1,17 @@
<script> <script>
import { ActionButton } from "@budibase/bbui" import { ActionButton } from "@budibase/bbui"
import GoogleLogo from "assets/google-logo.png" import GoogleLogo from "assets/google-logo.png"
import { admin } from "stores/portal" import { admin, auth } from "stores/portal"
let show = false let show = false
$: show = $admin.checklist?.oauth $: show = $admin.checklist?.oauth
$: tenantId = $auth.tenantId
</script> </script>
{#if show} {#if show}
<ActionButton <ActionButton
on:click={() => window.open("/api/admin/auth/google", "_blank")} on:click={() => window.open(`/api/admin/auth/${tenantId}/google`, "_blank")}
> >
<div class="inner"> <div class="inner">
<img src={GoogleLogo} alt="google icon" /> <img src={GoogleLogo} alt="google icon" />

View File

@ -7,6 +7,7 @@ export function createAuthStore() {
let initials = null let initials = null
let isAdmin = false let isAdmin = false
let isBuilder = false let isBuilder = false
let tenantId = "default"
if ($user) { if ($user) {
if ($user.firstName) { if ($user.firstName) {
initials = $user.firstName[0] initials = $user.firstName[0]
@ -20,18 +21,21 @@ export function createAuthStore() {
} }
isAdmin = !!$user.admin?.global isAdmin = !!$user.admin?.global
isBuilder = !!$user.builder?.global isBuilder = !!$user.builder?.global
tenantId = $user.tenantId || "default"
} }
return { return {
user: $user, user: $user,
initials, initials,
isAdmin, isAdmin,
isBuilder, isBuilder,
tenantId,
} }
}) })
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
checkAuth: async () => { checkAuth: async () => {
const response = await api.get("/api/admin/users/self") const response = await api.get("/api/admin/users/self")
if (response.status !== 200) { if (response.status !== 200) {
user.set(null) user.set(null)
@ -41,7 +45,8 @@ export function createAuthStore() {
} }
}, },
login: async creds => { login: async creds => {
const response = await api.post(`/api/admin/auth`, creds) const tenantId = get(store).tenantId
const response = await api.post(`/api/admin/auth/${tenantId}/login`, creds)
const json = await response.json() const json = await response.json()
if (response.status === 200) { if (response.status === 200) {
user.set(json.user) user.set(json.user)
@ -68,7 +73,8 @@ export function createAuthStore() {
} }
}, },
forgotPassword: async email => { forgotPassword: async email => {
const response = await api.post(`/api/admin/auth/reset`, { const tenantId = get(store).tenantId
const response = await api.post(`/api/admin/auth/${tenantId}/reset`, {
email, email,
}) })
if (response.status !== 200) { if (response.status !== 200) {
@ -77,7 +83,8 @@ export function createAuthStore() {
await response.json() await response.json()
}, },
resetPassword: async (password, code) => { resetPassword: async (password, code) => {
const response = await api.post(`/api/admin/auth/reset/update`, { const tenantId = get(store).tenantId
const response = await api.post(`/api/admin/auth/${tenantId}/reset/update`, {
password, password,
resetCode: code, resetCode: code,
}) })

View File

@ -2,7 +2,7 @@ import { writable, get } from "svelte/store"
import api from "builderStore/api" import api from "builderStore/api"
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
platformUrl: "http://localhost:1000", platformUrl: "http://localhost:10000",
logoUrl: undefined, logoUrl: undefined,
docsUrl: undefined, docsUrl: undefined,
company: "Budibase", company: "Budibase",

View File

@ -23,7 +23,7 @@ const { user: userCache } = require("@budibase/auth/cache")
const GLOBAL_USER_ID = "us_uuid1" const GLOBAL_USER_ID = "us_uuid1"
const EMAIL = "babs@babs.com" const EMAIL = "babs@babs.com"
const PASSWORD = "babs_password" const PASSWORD = "babs_password"
const TENANT_ID = "tenant1" const TENANT_ID = "default"
class TestConfiguration { class TestConfiguration {
constructor(openServer = true) { constructor(openServer = true) {

View File

@ -5,6 +5,7 @@ const {
getGlobalUserParams, getGlobalUserParams,
getScopedFullConfig, getScopedFullConfig,
getGlobalDBFromCtx, getGlobalDBFromCtx,
getGlobalDB,
getAllApps, getAllApps,
} = require("@budibase/auth/db") } = require("@budibase/auth/db")
const { Configs } = require("../../../constants") const { Configs } = require("../../../constants")
@ -168,7 +169,8 @@ exports.destroy = async function (ctx) {
} }
exports.configChecklist = async function (ctx) { exports.configChecklist = async function (ctx) {
const db = getGlobalDBFromCtx(ctx) const tenantId = ctx.query.tenantId
const db = tenantId ? getGlobalDB(tenantId) : getGlobalDBFromCtx(ctx)
try { try {
// TODO: Watch get started video // TODO: Watch get started video

View File

@ -56,7 +56,7 @@ async function saveUser(user, tenantId) {
// make sure another user isn't using the same email // make sure another user isn't using the same email
let dbUser let dbUser
if (email) { if (email) {
dbUser = await getGlobalUserByEmail(email) dbUser = await getGlobalUserByEmail(email, tenantId)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
throw "Email address already in use." throw "Email address already in use."
} }

View File

@ -1,18 +1,11 @@
const { DocumentTypes } = require("@budibase/auth").db const { getAllApps } = require("@budibase/auth/db")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const APP_PREFIX = "app_"
const URL_REGEX_SLASH = /\/|\\/g const URL_REGEX_SLASH = /\/|\\/g
exports.getApps = async ctx => { exports.getApps = async ctx => {
// allDbs call of CouchDB is very inaccurate in production const apps = await getAllApps({ CouchDB })
const allDbs = await CouchDB.allDbs()
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
const appPromises = appDbNames.map(db =>
new CouchDB(db).get(DocumentTypes.APP_METADATA)
)
const apps = await Promise.allSettled(appPromises)
const body = {} const body = {}
for (let app of apps) { for (let app of apps) {
if (app.status !== "fulfilled") { if (app.status !== "fulfilled") {

View File

@ -14,29 +14,29 @@ const PUBLIC_ENDPOINTS = [
method: "POST", method: "POST",
}, },
{ {
route: "/api/admin/auth", route: "/api/admin/auth/:tenantId/login",
method: "POST", method: "POST",
}, },
{ {
route: "/api/admin/auth/google", route: "/api/admin/auth/:tenantId/google",
method: "GET", method: "GET",
}, },
{ {
route: "/api/admin/auth/google/callback", route: "/api/admin/auth/:tenantId/google/callback",
method: "GET", method: "GET",
}, },
{ {
route: "/api/admin/auth/reset", route: "/api/admin/auth/:tenantId/reset",
method: "POST",
},
{
route: "/api/admin/auth/:tenantId/reset/update",
method: "POST", method: "POST",
}, },
{ {
route: "/api/admin/configs/checklist", route: "/api/admin/configs/checklist",
method: "GET", method: "GET",
}, },
{
route: "/api/apps",
method: "GET",
},
{ {
route: "/api/admin/configs/public", route: "/api/admin/configs/public",
method: "GET", method: "GET",

View File

@ -29,7 +29,7 @@ function buildResetUpdateValidation() {
} }
router router
.post("/api/admin/auth", buildAuthValidation(), authController.authenticate) .post("/api/admin/auth/:tenantId/login", buildAuthValidation(), authController.authenticate)
.post("/api/admin/auth/:tenantId/reset", buildResetValidation(), authController.reset) .post("/api/admin/auth/:tenantId/reset", buildResetValidation(), authController.reset)
.post( .post(
"/api/admin/auth/:tenantId/reset/update", "/api/admin/auth/:tenantId/reset/update",

View File

@ -7,6 +7,8 @@ const { Configs, LOGO_URL } = require("../../../../constants")
const { getGlobalUserByEmail } = require("@budibase/auth").utils const { getGlobalUserByEmail } = require("@budibase/auth").utils
const { createASession } = require("@budibase/auth/sessions") const { createASession } = require("@budibase/auth/sessions")
const TENANT_ID = "default"
class TestConfiguration { class TestConfiguration {
constructor(openServer = true) { constructor(openServer = true) {
if (openServer) { if (openServer) {
@ -72,6 +74,7 @@ class TestConfiguration {
_id: "us_uuid1", _id: "us_uuid1",
userId: "us_uuid1", userId: "us_uuid1",
sessionId: "sessionid", sessionId: "sessionid",
tenantId: TENANT_ID,
} }
const authToken = jwt.sign(user, env.JWT_SECRET) const authToken = jwt.sign(user, env.JWT_SECRET)
return { return {
@ -81,7 +84,7 @@ class TestConfiguration {
} }
async getUser(email) { async getUser(email) {
return getGlobalUserByEmail(email) return getGlobalUserByEmail(email, TENANT_ID)
} }
async createUser(email = "test@test.com", password = "test") { async createUser(email = "test@test.com", password = "test") {