Merge pull request #815 from Budibase/feature/auth-update
Updating authentication to work better with single tenancy
This commit is contained in:
commit
15a01e9b78
|
@ -1,3 +1,3 @@
|
|||
Cypress.Cookies.defaults({
|
||||
preserve: "builder:token",
|
||||
preserve: "budibase:builder:local",
|
||||
})
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import { store } from "./index"
|
||||
import { get as svelteGet } from "svelte/store"
|
||||
|
||||
const apiCall = method => async (
|
||||
url,
|
||||
body,
|
||||
headers = { "Content-Type": "application/json" }
|
||||
) => {
|
||||
const response = await fetch(url, {
|
||||
headers["x-budibase-app-id"] = svelteGet(store).appId
|
||||
return await fetch(url, {
|
||||
method: method,
|
||||
body: body && JSON.stringify(body),
|
||||
headers,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export const post = apiCall("POST")
|
||||
|
|
|
@ -84,7 +84,6 @@
|
|||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
document.cookie = 'budibase:token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
|
||||
window.open(`/${application}`)
|
||||
}}>
|
||||
Preview
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { authenticate } from "./authenticate"
|
||||
// import appStore from "../state/store"
|
||||
import { getAppIdFromPath } from "../render/getAppId"
|
||||
|
||||
const apiCall = method => async ({ url, body }) => {
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-budibase-app-id": getAppIdFromPath(),
|
||||
},
|
||||
body: body && JSON.stringify(body),
|
||||
credentials: "same-origin",
|
||||
|
@ -36,9 +37,8 @@ const del = apiCall("DELETE")
|
|||
|
||||
const ERROR_MEMBER = "##error"
|
||||
const error = message => {
|
||||
const err = { [ERROR_MEMBER]: message }
|
||||
// appStore.update(s => s["##error_message"], message)
|
||||
return err
|
||||
return { [ERROR_MEMBER]: message }
|
||||
}
|
||||
|
||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
||||
|
@ -80,7 +80,7 @@ const makeRowRequestBody = (parameters, state) => {
|
|||
if (body._table) delete body._table
|
||||
|
||||
// then override with supplied parameters
|
||||
for (let fieldName in parameters.fields) {
|
||||
for (let fieldName of Object.keys(parameters.fields)) {
|
||||
const field = parameters.fields[fieldName]
|
||||
|
||||
// ensure fields sent are of the correct type
|
||||
|
|
|
@ -2,7 +2,7 @@ import { attachChildren } from "./render/attachChildren"
|
|||
import { createTreeNode } from "./render/prepareRenderComponent"
|
||||
import { screenRouter } from "./render/screenRouter"
|
||||
import { createStateManager } from "./state/stateManager"
|
||||
import { parseAppIdFromCookie } from "./render/getAppId"
|
||||
import { getAppIdFromPath } from "./render/getAppId"
|
||||
|
||||
export const createApp = ({
|
||||
componentLibraries,
|
||||
|
@ -38,7 +38,7 @@ export const createApp = ({
|
|||
window,
|
||||
})
|
||||
const fallbackPath = window.location.pathname.replace(
|
||||
parseAppIdFromCookie(window.document.cookie),
|
||||
getAppIdFromPath(),
|
||||
""
|
||||
)
|
||||
routeTo(currentUrl || fallbackPath)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createApp } from "./createApp"
|
||||
import { builtins, builtinLibName } from "./render/builtinComponents"
|
||||
import { parseAppIdFromCookie } from "./render/getAppId"
|
||||
import { getAppIdFromPath } from "./render/getAppId"
|
||||
|
||||
/**
|
||||
* create a web application from static budibase definition files.
|
||||
|
@ -9,7 +9,7 @@ import { parseAppIdFromCookie } from "./render/getAppId"
|
|||
export const loadBudibase = async opts => {
|
||||
const _window = (opts && opts.window) || window
|
||||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
||||
const appId = parseAppIdFromCookie(_window.document.cookie)
|
||||
const appId = getAppIdFromPath()
|
||||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
||||
|
||||
const user = {}
|
||||
|
|
|
@ -1,14 +1,4 @@
|
|||
export const parseAppIdFromCookie = docCookie => {
|
||||
const cookie =
|
||||
docCookie.split(";").find(c => c.trim().startsWith("budibase:token")) ||
|
||||
docCookie.split(";").find(c => c.trim().startsWith("builder:token"))
|
||||
|
||||
if (!cookie) return location.pathname.replace(/\//g, "")
|
||||
|
||||
const base64Token = cookie.substring(lengthOfKey)
|
||||
|
||||
const user = JSON.parse(atob(base64Token.split(".")[1]))
|
||||
return user.appId
|
||||
export const getAppIdFromPath = () => {
|
||||
let appId = location.pathname.split("/")[1]
|
||||
return appId && appId.startsWith("app_") ? appId : undefined
|
||||
}
|
||||
|
||||
const lengthOfKey = "budibase:token=".length
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import regexparam from "regexparam"
|
||||
import appStore from "../state/store"
|
||||
import { parseAppIdFromCookie } from "./getAppId"
|
||||
import { getAppIdFromPath } from "./getAppId"
|
||||
|
||||
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
||||
function sanitize(url) {
|
||||
|
@ -27,7 +27,7 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
|||
|
||||
const makeRootedPath = url => {
|
||||
if (isRunningLocally()) {
|
||||
const appId = parseAppIdFromCookie(window.document.cookie)
|
||||
const appId = getAppIdFromPath()
|
||||
if (url) {
|
||||
url = sanitize(url)
|
||||
if (!url.startsWith("/")) {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { load, makePage, makeScreen, walkComponentTree } from "./testAppDef"
|
||||
import { isScreenSlot } from "../src/render/builtinComponents"
|
||||
jest.mock("../src/render/getAppId", () => ({
|
||||
getAppIdFromPath: () => "TEST_APP_ID"
|
||||
}))
|
||||
|
||||
describe("screenRouting", () => {
|
||||
it("should load correct screen, for initial URL", async () => {
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import jsdom, { JSDOM } from "jsdom"
|
||||
import { loadBudibase } from "../src/index"
|
||||
|
||||
export const APP_ID = "TEST_APP_ID"
|
||||
|
||||
export const load = async (page, screens, url, host = "test.com") => {
|
||||
screens = screens || []
|
||||
url = url || "/"
|
||||
|
||||
const fullUrl = `http://${host}${url}`
|
||||
const cookieJar = new jsdom.CookieJar()
|
||||
const cookie = `${btoa("{}")}.${btoa('{"appId":"TEST_APP_ID"}')}.signature`
|
||||
const cookie = `${btoa("{}")}.${btoa(`{"appId":"${APP_ID}"}`)}.signature`
|
||||
cookieJar.setCookie(
|
||||
`budibase:token=${cookie};domain=${host};path=/`,
|
||||
`budibase:${APP_ID}:local=${cookie};domain=${host};path=/`,
|
||||
fullUrl,
|
||||
{
|
||||
looseMode: false,
|
||||
|
|
|
@ -61,7 +61,7 @@ exports.fetchAppPackage = async function(ctx) {
|
|||
const db = new CouchDB(ctx.params.appId)
|
||||
const application = await db.get(ctx.params.appId)
|
||||
ctx.body = await getPackageForBuilder(ctx.config, application)
|
||||
setBuilderToken(ctx, ctx.params.appId, application.version)
|
||||
await setBuilderToken(ctx, ctx.params.appId, application.version)
|
||||
}
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
|
@ -70,7 +70,6 @@ exports.create = async function(ctx) {
|
|||
const newApplication = {
|
||||
_id: appId,
|
||||
type: "app",
|
||||
userInstanceMap: {},
|
||||
version: packageJson.version,
|
||||
componentLibraries: ["@budibase/standard-components"],
|
||||
name: ctx.request.body.name,
|
||||
|
|
|
@ -4,9 +4,10 @@ const bcrypt = require("../../utilities/bcrypt")
|
|||
const env = require("../../environment")
|
||||
const { getAPIKey } = require("../../utilities/usageQuota")
|
||||
const { generateUserID } = require("../../db/utils")
|
||||
const { setCookie } = require("../../utilities")
|
||||
|
||||
exports.authenticate = async ctx => {
|
||||
const appId = ctx.user.appId
|
||||
const appId = ctx.appId
|
||||
if (!appId) ctx.throw(400, "No appId")
|
||||
|
||||
const { username, password } = ctx.request.body
|
||||
|
@ -33,7 +34,6 @@ exports.authenticate = async ctx => {
|
|||
userId: dbUser._id,
|
||||
accessLevelId: dbUser.accessLevelId,
|
||||
version: app.version,
|
||||
appId,
|
||||
}
|
||||
// if in cloud add the user api key
|
||||
if (env.CLOUD) {
|
||||
|
@ -45,19 +45,13 @@ exports.authenticate = async ctx => {
|
|||
expiresIn: "1 day",
|
||||
})
|
||||
|
||||
const expires = new Date()
|
||||
expires.setDate(expires.getDate() + 1)
|
||||
|
||||
ctx.cookies.set("budibase:token", token, {
|
||||
expires,
|
||||
path: "/",
|
||||
httpOnly: false,
|
||||
overwrite: true,
|
||||
})
|
||||
setCookie(ctx, appId, token)
|
||||
|
||||
delete dbUser.password
|
||||
ctx.body = {
|
||||
token,
|
||||
...dbUser,
|
||||
appId,
|
||||
}
|
||||
} else {
|
||||
ctx.throw(401, "Invalid credentials.")
|
||||
|
|
|
@ -21,7 +21,7 @@ const COMP_LIB_BASE_APP_VERSION = "0.2.5"
|
|||
exports.serveBuilder = async function(ctx) {
|
||||
let builderPath = resolve(__dirname, "../../../builder")
|
||||
if (ctx.file === "index.html") {
|
||||
setBuilderToken(ctx)
|
||||
await setBuilderToken(ctx)
|
||||
}
|
||||
await send(ctx, ctx.file, { root: ctx.devPath || builderPath })
|
||||
}
|
||||
|
|
|
@ -39,13 +39,6 @@ exports.create = async function(ctx) {
|
|||
|
||||
const response = await db.post(user)
|
||||
|
||||
const app = await db.get(ctx.user.appId)
|
||||
app.userInstanceMap = {
|
||||
...app.userInstanceMap,
|
||||
[username]: ctx.user.appId,
|
||||
}
|
||||
await db.put(app)
|
||||
|
||||
ctx.status = 200
|
||||
ctx.message = "User created successfully."
|
||||
ctx.userId = response._id
|
||||
|
|
|
@ -27,15 +27,19 @@ exports.defaultHeaders = appId => {
|
|||
const builderUser = {
|
||||
userId: "BUILDER",
|
||||
accessLevelId: BUILDER_LEVEL_ID,
|
||||
appId,
|
||||
}
|
||||
|
||||
const builderToken = jwt.sign(builderUser, env.JWT_SECRET)
|
||||
|
||||
return {
|
||||
const headers = {
|
||||
Accept: "application/json",
|
||||
Cookie: [`builder:token=${builderToken}`],
|
||||
Cookie: [`budibase:builder:local=${builderToken}`],
|
||||
}
|
||||
if (appId) {
|
||||
headers["x-budibase-app-id"] = appId
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
exports.createTable = async (request, appId, table) => {
|
||||
|
@ -209,7 +213,10 @@ const createUserWithPermissions = async (
|
|||
|
||||
const loginResult = await request
|
||||
.post(`/api/authenticate`)
|
||||
.set({ Cookie: `budibase:token=${anonToken}` })
|
||||
.set({
|
||||
Cookie: `budibase:${appId}:local=${anonToken}`,
|
||||
"x-budibase-app-id": appId,
|
||||
})
|
||||
.send({ username, password })
|
||||
|
||||
// returning necessary request headers
|
||||
|
|
|
@ -9,6 +9,7 @@ const {
|
|||
} = require("../utilities/accessLevels")
|
||||
const env = require("../environment")
|
||||
const { AuthTypes } = require("../constants")
|
||||
const { getAppId, getCookieName, setCookie } = require("../utilities")
|
||||
|
||||
module.exports = async (ctx, next) => {
|
||||
if (ctx.path === "/_builder") {
|
||||
|
@ -16,8 +17,18 @@ module.exports = async (ctx, next) => {
|
|||
return
|
||||
}
|
||||
|
||||
const appToken = ctx.cookies.get("budibase:token")
|
||||
const builderToken = ctx.cookies.get("builder:token")
|
||||
// do everything we can to make sure the appId is held correctly
|
||||
// we hold it in state as a
|
||||
let appId = getAppId(ctx)
|
||||
const cookieAppId = ctx.cookies.get(getCookieName("currentapp"))
|
||||
if (appId && cookieAppId !== appId) {
|
||||
setCookie(ctx, "currentapp", appId)
|
||||
} else if (cookieAppId) {
|
||||
appId = cookieAppId
|
||||
}
|
||||
|
||||
const appToken = ctx.cookies.get(getCookieName(appId))
|
||||
const builderToken = ctx.cookies.get(getCookieName())
|
||||
|
||||
let token
|
||||
// if running locally in the builder itself
|
||||
|
@ -31,16 +42,6 @@ module.exports = async (ctx, next) => {
|
|||
|
||||
if (!token) {
|
||||
ctx.auth.authenticated = false
|
||||
|
||||
let appId = env.CLOUD ? ctx.subdomains[1] : ctx.params.appId
|
||||
|
||||
// if appId can't be determined from path param or subdomain
|
||||
if (!appId && ctx.request.headers.referer) {
|
||||
const url = new URL(ctx.request.headers.referer)
|
||||
// remove leading and trailing slashes from appId
|
||||
appId = url.pathname.replace(/\//g, "")
|
||||
}
|
||||
|
||||
ctx.user = {
|
||||
appId,
|
||||
}
|
||||
|
@ -50,14 +51,12 @@ module.exports = async (ctx, next) => {
|
|||
|
||||
try {
|
||||
const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
|
||||
ctx.appId = appId
|
||||
ctx.auth.apiKey = jwtPayload.apiKey
|
||||
ctx.user = {
|
||||
...jwtPayload,
|
||||
appId: jwtPayload.appId,
|
||||
accessLevel: await getAccessLevel(
|
||||
jwtPayload.appId,
|
||||
jwtPayload.accessLevelId
|
||||
),
|
||||
appId: appId,
|
||||
accessLevel: await getAccessLevel(appId, jwtPayload.accessLevelId),
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
const { BUILDER_LEVEL_ID } = require("../accessLevels")
|
||||
const env = require("../../environment")
|
||||
const CouchDB = require("../../db")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const { DocumentTypes, SEPARATOR } = require("../../db/utils")
|
||||
const { setCookie } = require("../index")
|
||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||
|
||||
module.exports = (ctx, appId, version) => {
|
||||
module.exports = async (ctx, appId, version) => {
|
||||
const builderUser = {
|
||||
userId: "BUILDER",
|
||||
accessLevelId: BUILDER_LEVEL_ID,
|
||||
appId,
|
||||
version,
|
||||
}
|
||||
if (env.BUDIBASE_API_KEY) {
|
||||
|
@ -16,16 +19,13 @@ module.exports = (ctx, appId, version) => {
|
|||
expiresIn: "30 days",
|
||||
})
|
||||
|
||||
const expiry = new Date()
|
||||
expiry.setDate(expiry.getDate() + 30)
|
||||
// remove the app token
|
||||
ctx.cookies.set("budibase:token", "", {
|
||||
overwrite: true,
|
||||
})
|
||||
// set the builder token
|
||||
ctx.cookies.set("builder:token", token, {
|
||||
expires: expiry,
|
||||
httpOnly: false,
|
||||
overwrite: true,
|
||||
setCookie(ctx, "builder", token)
|
||||
// need to clear all app tokens or else unable to use the app in the builder
|
||||
let allDbNames = await CouchDB.allDbs()
|
||||
allDbNames.map(dbName => {
|
||||
if (dbName.startsWith(APP_PREFIX)) {
|
||||
setCookie(ctx, dbName, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const env = require("../environment")
|
||||
const { DocumentTypes, SEPARATOR } = require("../db/utils")
|
||||
|
||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||
|
||||
exports.wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
|
@ -10,3 +13,54 @@ exports.isDev = () => {
|
|||
env.NODE_ENV !== "cypress"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request tries to find the appId, which can be located in various places
|
||||
* @param {object} ctx The main request body to look through.
|
||||
* @returns {string|undefined} If an appId was found it will be returned.
|
||||
*/
|
||||
exports.getAppId = ctx => {
|
||||
let appId = ctx.headers["x-budibase-app-id"]
|
||||
if (!appId) {
|
||||
appId = env.CLOUD ? ctx.subdomains[1] : ctx.params.appId
|
||||
}
|
||||
// look in body if can't find it in subdomain
|
||||
if (!appId && ctx.request.body && ctx.request.body.appId) {
|
||||
appId = ctx.request.body.appId
|
||||
}
|
||||
let appPath =
|
||||
ctx.request.headers.referrer ||
|
||||
ctx.path.split("/").filter(subPath => subPath.startsWith(APP_PREFIX))
|
||||
if (!appId && appPath.length !== 0) {
|
||||
appId = appPath[0]
|
||||
}
|
||||
return appId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the cookie which is to be updated/retrieved
|
||||
* @param {string|undefined|null} name OPTIONAL can specify the specific app if previewing etc
|
||||
* @returns {string} The name of the token trying to find
|
||||
*/
|
||||
exports.getCookieName = (name = "builder") => {
|
||||
let environment = env.CLOUD ? "cloud" : "local"
|
||||
return `budibase:${name}:${environment}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a cookie for the request, has a hardcoded expiry.
|
||||
* @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, name, value) => {
|
||||
const expires = new Date()
|
||||
expires.setDate(expires.getDate() + 1)
|
||||
|
||||
ctx.cookies.set(exports.getCookieName(name), value, {
|
||||
expires,
|
||||
path: "/",
|
||||
httpOnly: false,
|
||||
overwrite: true,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -18,9 +18,14 @@
|
|||
}
|
||||
|
||||
const logOut = () => {
|
||||
document.cookie =
|
||||
"budibase:token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"
|
||||
location.reload()
|
||||
// TODO: not the best way to clear cookie, try to find better way
|
||||
const appId = location.pathname.split("/")[1]
|
||||
if (appId) {
|
||||
for (let environment of ["local", "cloud"]) {
|
||||
document.cookie = `budibase:${appId}:${environment}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`
|
||||
}
|
||||
}
|
||||
location.href = `/${appId}`
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -5,6 +5,10 @@ const apiCall = method => async (
|
|||
"Content-Type": "application/json",
|
||||
}
|
||||
) => {
|
||||
const appId = location.pathname.split("/")[1]
|
||||
if (appId) {
|
||||
headers["x-budibase-app-id"] = appId
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
body: body && JSON.stringify(body),
|
||||
|
|
Loading…
Reference in New Issue