Merge pull request #2901 from Budibase/fix/custom-sso-enforce-cross-tenancy

Don't let user exist in multiple tenants when using custom sso
This commit is contained in:
Martin McKeaveney 2021-10-06 16:14:34 +01:00 committed by GitHub
commit 2f5ab466b0
5 changed files with 139 additions and 118 deletions

View File

@ -20,6 +20,10 @@ const getErrorMessage = () => {
return done.mock.calls[0][2].message return done.mock.calls[0][2].message
} }
const saveUser = async (user) => {
return await db.put(user)
}
describe("third party common", () => { describe("third party common", () => {
describe("authenticateThirdParty", () => { describe("authenticateThirdParty", () => {
let thirdPartyUser let thirdPartyUser
@ -36,7 +40,7 @@ describe("third party common", () => {
describe("validation", () => { describe("validation", () => {
const testValidation = async (message) => { const testValidation = async (message) => {
await authenticateThirdParty(thirdPartyUser, false, done) await authenticateThirdParty(thirdPartyUser, false, done, saveUser)
expect(done.mock.calls.length).toBe(1) expect(done.mock.calls.length).toBe(1)
expect(getErrorMessage()).toContain(message) expect(getErrorMessage()).toContain(message)
} }
@ -78,7 +82,7 @@ describe("third party common", () => {
describe("when the user doesn't exist", () => { describe("when the user doesn't exist", () => {
describe("when a local account is required", () => { describe("when a local account is required", () => {
it("returns an error message", async () => { it("returns an error message", async () => {
await authenticateThirdParty(thirdPartyUser, true, done) await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
expect(done.mock.calls.length).toBe(1) expect(done.mock.calls.length).toBe(1)
expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.") expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.")
}) })
@ -86,7 +90,7 @@ describe("third party common", () => {
describe("when a local account isn't required", () => { describe("when a local account isn't required", () => {
it("creates and authenticates the user", async () => { it("creates and authenticates the user", async () => {
await authenticateThirdParty(thirdPartyUser, false, done) await authenticateThirdParty(thirdPartyUser, false, done, saveUser)
const user = expectUserIsAuthenticated() const user = expectUserIsAuthenticated()
expectUserIsSynced(user, thirdPartyUser) expectUserIsSynced(user, thirdPartyUser)
expect(user.roles).toStrictEqual({}) expect(user.roles).toStrictEqual({})
@ -123,7 +127,7 @@ describe("third party common", () => {
}) })
it("syncs and authenticates the user", async () => { it("syncs and authenticates the user", async () => {
await authenticateThirdParty(thirdPartyUser, true, done) await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
const user = expectUserIsAuthenticated() const user = expectUserIsAuthenticated()
expectUserIsSynced(user, thirdPartyUser) expectUserIsSynced(user, thirdPartyUser)
@ -139,7 +143,7 @@ describe("third party common", () => {
}) })
it("syncs and authenticates the user", async () => { it("syncs and authenticates the user", async () => {
await authenticateThirdParty(thirdPartyUser, true, done) await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
const user = expectUserIsAuthenticated() const user = expectUserIsAuthenticated()
expectUserIsSynced(user, thirdPartyUser) expectUserIsSynced(user, thirdPartyUser)

View File

@ -1,6 +1,7 @@
const env = require("../../environment") const env = require("../../environment")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const { generateGlobalUserID } = require("../../db/utils") const { generateGlobalUserID } = require("../../db/utils")
const { saveUser } = require("../../utils")
const { authError } = require("./utils") const { authError } = require("./utils")
const { newid } = require("../../hashing") const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions") const { createASession } = require("../../security/sessions")
@ -14,7 +15,8 @@ const fetch = require("node-fetch")
exports.authenticateThirdParty = async function ( exports.authenticateThirdParty = async function (
thirdPartyUser, thirdPartyUser,
requireLocalAccount = true, requireLocalAccount = true,
done done,
saveUserFn = saveUser
) { ) {
if (!thirdPartyUser.provider) { if (!thirdPartyUser.provider) {
return authError(done, "third party user provider required") return authError(done, "third party user provider required")
@ -71,7 +73,13 @@ exports.authenticateThirdParty = async function (
dbUser = await syncUser(dbUser, thirdPartyUser) dbUser = await syncUser(dbUser, thirdPartyUser)
// create or sync the user // create or sync the user
const response = await db.put(dbUser) let response
try {
response = await saveUserFn(dbUser, getTenantId(), false, false)
} catch (err) {
return authError(done, err)
}
dbUser._rev = response.rev dbUser._rev = response.rev
// authenticate // authenticate

View File

@ -107,3 +107,13 @@ exports.lookupTenantId = async userId => {
} }
return tenantId return tenantId
} }
// lookup, could be email or userId, either will return a doc
exports.getTenantUser = async identifier => {
const db = getDB(PLATFORM_INFO_DB)
try {
return await db.get(identifier)
} catch (err) {
return null
}
}

View File

@ -1,10 +1,24 @@
const { DocumentTypes, SEPARATOR, ViewNames } = require("./db/utils") const {
DocumentTypes,
SEPARATOR,
ViewNames,
generateGlobalUserID,
} = require("./db/utils")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt") const { options } = require("./middleware/passport/jwt")
const { createUserEmailView } = require("./db/views") const { createUserEmailView } = require("./db/views")
const { Headers } = require("./constants") const { Headers, UserStatus } = require("./constants")
const { getGlobalDB } = require("./tenancy") const {
getGlobalDB,
updateTenantId,
getTenantUser,
tryAddTenant,
} = require("./tenancy")
const environment = require("./environment") const environment = require("./environment")
const accounts = require("./cloud/accounts")
const { hash } = require("./hashing")
const userCache = require("./cache/user")
const env = require("./environment")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -131,3 +145,93 @@ exports.getGlobalUserByEmail = async email => {
} }
} }
} }
exports.saveUser = async (
user,
tenantId,
hashPassword = true,
requirePassword = true
) => {
if (!tenantId) {
throw "No tenancy specified."
}
// need to set the context for this request, as specified
updateTenantId(tenantId)
// specify the tenancy incase we're making a new admin user (public)
const db = getGlobalDB(tenantId)
let { email, password, _id } = user
// make sure another user isn't using the same email
let dbUser
if (email) {
// check budibase users inside the tenant
dbUser = await exports.getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
throw `Email address ${email} already in use.`
}
// check budibase users in other tenants
if (env.MULTI_TENANCY) {
dbUser = await getTenantUser(email)
if (dbUser != null && dbUser.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
// check root account users in account portal
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(email)
if (account && account.verified && account.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
} else {
dbUser = await db.get(_id)
}
// get the password, make sure one is defined
let hashedPassword
if (password) {
hashedPassword = hashPassword ? await hash(password) : password
} else if (dbUser) {
hashedPassword = dbUser.password
} else if (requirePassword) {
throw "Password must be specified."
}
_id = _id || generateGlobalUserID()
user = {
createdAt: Date.now(),
...dbUser,
...user,
_id,
password: hashedPassword,
tenantId,
}
// make sure the roles object is always present
if (!user.roles) {
user.roles = {}
}
// add the active status to a user if its not provided
if (user.status == null) {
user.status = UserStatus.ACTIVE
}
try {
const response = await db.put({
password: hashedPassword,
...user,
})
await tryAddTenant(tenantId, _id, email)
await userCache.invalidateUser(response.id)
return {
_id: response.id,
_rev: response.rev,
email,
}
} catch (err) {
if (err.status === 409) {
throw "User exists already"
} else {
throw err
}
}
}

View File

@ -1,29 +1,24 @@
const { const {
generateGlobalUserID,
getGlobalUserParams, getGlobalUserParams,
StaticDatabases, StaticDatabases,
generateNewUsageQuotaDoc, generateNewUsageQuotaDoc,
} = require("@budibase/auth/db") } = require("@budibase/auth/db")
const { hash, getGlobalUserByEmail } = require("@budibase/auth").utils const { hash, getGlobalUserByEmail, saveUser } = require("@budibase/auth").utils
const { UserStatus, EmailTemplatePurpose } = require("../../../constants") const { 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 { user: userCache } = require("@budibase/auth/cache")
const { invalidateSessions } = require("@budibase/auth/sessions") const { invalidateSessions } = require("@budibase/auth/sessions")
const CouchDB = require("../../../db")
const accounts = require("@budibase/auth/accounts") const accounts = require("@budibase/auth/accounts")
const { const {
getGlobalDB, getGlobalDB,
getTenantId, getTenantId,
getTenantUser,
doesTenantExist, doesTenantExist,
tryAddTenant,
updateTenantId,
} = require("@budibase/auth/tenancy") } = require("@budibase/auth/tenancy")
const { removeUserFromInfoDB } = require("@budibase/auth/deprovision") const { removeUserFromInfoDB } = require("@budibase/auth/deprovision")
const env = require("../../../environment") const env = require("../../../environment")
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
async function allUsers() { async function allUsers() {
const db = getGlobalDB() const db = getGlobalDB()
const response = await db.allDocs( const response = await db.allDocs(
@ -34,96 +29,6 @@ async function allUsers() {
return response.rows.map(row => row.doc) return response.rows.map(row => row.doc)
} }
async function saveUser(
user,
tenantId,
hashPassword = true,
requirePassword = true
) {
if (!tenantId) {
throw "No tenancy specified."
}
// need to set the context for this request, as specified
updateTenantId(tenantId)
// specify the tenancy incase we're making a new admin user (public)
const db = getGlobalDB(tenantId)
let { email, password, _id } = user
// make sure another user isn't using the same email
let dbUser
if (email) {
// check budibase users inside the tenant
dbUser = await getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
throw `Email address ${email} already in use.`
}
// check budibase users in other tenants
if (env.MULTI_TENANCY) {
dbUser = await getTenantUser(email)
if (dbUser != null && dbUser.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
// check root account users in account portal
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(email)
if (account && account.verified && account.tenantId !== tenantId) {
throw `Email address ${email} already in use.`
}
}
} else {
dbUser = await db.get(_id)
}
// get the password, make sure one is defined
let hashedPassword
if (password) {
hashedPassword = hashPassword ? await hash(password) : password
} else if (dbUser) {
hashedPassword = dbUser.password
} else if (requirePassword) {
throw "Password must be specified."
}
_id = _id || generateGlobalUserID()
user = {
createdAt: Date.now(),
...dbUser,
...user,
_id,
password: hashedPassword,
tenantId,
}
// make sure the roles object is always present
if (!user.roles) {
user.roles = {}
}
// add the active status to a user if its not provided
if (user.status == null) {
user.status = UserStatus.ACTIVE
}
try {
const response = await db.put({
password: hashedPassword,
...user,
})
await tryAddTenant(tenantId, _id, email)
await userCache.invalidateUser(response.id)
return {
_id: response.id,
_rev: response.rev,
email,
}
} catch (err) {
if (err.status === 409) {
throw "User exists already"
} else {
throw err
}
}
}
exports.save = async ctx => { exports.save = async ctx => {
try { try {
ctx.body = await saveUser(ctx.request.body, getTenantId()) ctx.body = await saveUser(ctx.request.body, getTenantId())
@ -310,16 +215,6 @@ exports.find = async ctx => {
ctx.body = user ctx.body = user
} }
// lookup, could be email or userId, either will return a doc
const getTenantUser = async identifier => {
const db = new CouchDB(PLATFORM_INFO_DB)
try {
return await db.get(identifier)
} catch (err) {
return null
}
}
exports.tenantUserLookup = async ctx => { exports.tenantUserLookup = async ctx => {
const id = ctx.params.id const id = ctx.params.id
const user = await getTenantUser(id) const user = await getTenantUser(id)