Skip to content

Commit

Permalink
Merge pull request #815 from Budibase/feature/auth-update
Browse files Browse the repository at this point in the history
Updating authentication to work better with single tenancy
  • Loading branch information
Michael Drury authored Nov 4, 2020
2 parents a874399 + 016e09e commit 15a01e9
Show file tree
Hide file tree
Showing 20 changed files with 138 additions and 87 deletions.
2 changes: 1 addition & 1 deletion packages/builder/cypress/support/cookies.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Cypress.Cookies.defaults({
preserve: "builder:token",
preserve: "budibase:builder:local",
})
8 changes: 5 additions & 3 deletions packages/builder/src/builderStore/api.js
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
1 change: 0 additions & 1 deletion packages/builder/src/pages/[application]/_reset.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/client/src/api/index.js
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/createApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -38,7 +38,7 @@ export const createApp = ({
window,
})
const fallbackPath = window.location.pathname.replace(
parseAppIdFromCookie(window.document.cookie),
getAppIdFromPath(),
""
)
routeTo(currentUrl || fallbackPath)
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/index.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 = {}
Expand Down
16 changes: 3 additions & 13 deletions packages/client/src/render/getAppId.js
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions packages/client/src/render/screenRouter.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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("/")) {
Expand Down
3 changes: 3 additions & 0 deletions packages/client/tests/screenRouting.spec.js
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/client/tests/testAppDef.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
3 changes: 1 addition & 2 deletions packages/server/src/api/controllers/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
16 changes: 5 additions & 11 deletions packages/server/src/api/controllers/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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.")
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/api/controllers/static.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down
7 changes: 0 additions & 7 deletions packages/server/src/api/controllers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions packages/server/src/api/routes/tests/couchTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down
33 changes: 16 additions & 17 deletions packages/server/src/middleware/authenticated.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,26 @@ 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") {
await 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
Expand All @@ -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,
}
Expand All @@ -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)
Expand Down
24 changes: 12 additions & 12 deletions packages/server/src/utilities/builder/setBuilderToken.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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, "")
}
})
}
Loading

0 comments on commit 15a01e9

Please sign in to comment.