Some work towards implementing the current app cookie, removing some old dead code and re-working some of the different middlewares involved.

This commit is contained in:
mike12345567 2021-04-12 18:31:58 +01:00
parent 0e583eb185
commit eaad867780
11 changed files with 95 additions and 136 deletions

View File

@ -22,6 +22,10 @@ exports.generateUserID = email => {
return `${DocumentTypes.USER}${SEPARATOR}${email}` return `${DocumentTypes.USER}${SEPARATOR}${email}`
} }
exports.getEmailFromUserID = userId => {
return userId.split(`${DocumentTypes.USER}${SEPARATOR}`)[1]
}
/** /**
* Gets parameters for retrieving users, this is a utility function for the getDocParams function. * Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/ */

View File

@ -7,7 +7,7 @@ const { StaticDatabases } = require("./db/utils")
const { jwt, local, google, authenticated } = require("./middleware") const { jwt, local, google, authenticated } = require("./middleware")
const { Cookies, UserStatus } = require("./constants") const { Cookies, UserStatus } = require("./constants")
const { hash, compare } = require("./hashing") const { hash, compare } = require("./hashing")
const { getAppId, setCookie } = require("./utils") const { getAppId, setCookie, getCookie } = require("./utils")
const { const {
generateUserID, generateUserID,
getUserParams, getUserParams,
@ -45,5 +45,6 @@ module.exports = {
compare, compare,
getAppId, getAppId,
setCookie, setCookie,
getCookie,
authenticated, authenticated,
} }

View File

@ -1,57 +1,21 @@
const CouchDB = require("../db")
const { Cookies } = require("../constants") const { Cookies } = require("../constants")
const { getAppId, setCookie, getCookie } = require("../utils") const { getCookie } = require("../utils")
const { StaticDatabases } = require("../db/utils") const { getEmailFromUserID } = require("../db/utils")
async function setCurrentAppContext(ctx) {
let role = "PUBLIC"
// Current app cookie
let appId = getAppId(ctx)
if (!appId) {
ctx.user = {
role,
}
return
}
console.log("THE APP ID", appId)
const currentAppCookie = getCookie(ctx, Cookies.CurrentApp, { decrypt: true })
const appIdChanged = appId && currentAppCookie.appId !== appId
if (appIdChanged) {
try {
// get roles for user from global DB
const db = new CouchDB(StaticDatabases.USER)
const user = await db.get(ctx.user)
role = user.roles[appId]
} catch (err) {
// no user exists
}
} else if (currentAppCookie.appId) {
appId = currentAppCookie.appId
}
setCookie(ctx, { appId, role }, Cookies.CurrentApp, { encrypt: true })
return appId
}
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
try { try {
// check the actual user is authenticated first // check the actual user is authenticated first
const authCookie = getCookie(ctx, Cookies.Auth, { decrypt: true }) const authCookie = getCookie(ctx, Cookies.Auth)
if (authCookie) { if (authCookie) {
ctx.isAuthenticated = true ctx.isAuthenticated = true
ctx.user = authCookie._id ctx.user = authCookie
// make sure email is correct from ID
ctx.user.email = getEmailFromUserID(authCookie._id)
} }
ctx.appId = await setCurrentAppContext(ctx)
console.log("CONTEXT", ctx)
await next() await next()
} catch (err) { } catch (err) {
console.log(err)
ctx.throw(err.status || 403, err.text) ctx.throw(err.status || 403, err.text)
} }
} }

View File

@ -1,9 +1,4 @@
const { Cookies } = require("../../constants")
exports.options = { exports.options = {
jwtFromRequest: function(ctx) {
return ctx.cookies.get(Cookies.Auth)
},
secretOrKey: process.env.JWT_SECRET, secretOrKey: process.env.JWT_SECRET,
} }

View File

@ -45,17 +45,15 @@ exports.getAppId = ctx => {
* Get a cookie from context, and decrypt if necessary. * Get a cookie from context, and decrypt if necessary.
* @param {object} ctx The request which is to be manipulated. * @param {object} ctx The request which is to be manipulated.
* @param {string} name The name of the cookie to get. * @param {string} name The name of the cookie to get.
* @param {object} options options .
*/ */
exports.getCookie = (ctx, value, options = {}) => { exports.getCookie = (ctx, name) => {
const cookie = ctx.cookies.get(value) const cookie = ctx.cookies.get(name)
if (!cookie) return if (!cookie) {
return cookie
}
if (!options.decrypt) return cookie return jwt.verify(cookie, options.secretOrKey)
const payload = jwt.verify(cookie, process.env.JWT_SECRET)
return payload
} }
/** /**
@ -71,11 +69,9 @@ exports.setCookie = (ctx, value, name = "builder") => {
if (!value) { if (!value) {
ctx.cookies.set(name) ctx.cookies.set(name)
} else { } else {
if (options.encrypt) { value = jwt.sign(value, options.secretOrKey, {
value = jwt.sign(value, process.env.JWT_SECRET, {
expiresIn: "1 day", expiresIn: "1 day",
}) })
}
ctx.cookies.set(name, value, { ctx.cookies.set(name, value, {
expires, expires,
path: "/", path: "/",

View File

@ -1,5 +1,6 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const { authenticated } = require("@budibase/auth") const { authenticated } = require("@budibase/auth")
const currentApp = require("../middleware/currentapp")
const compress = require("koa-compress") const compress = require("koa-compress")
const zlib = require("zlib") const zlib = require("zlib")
const { mainRoutes, authRoutes, staticRoutes } = require("./routes") const { mainRoutes, authRoutes, staticRoutes } = require("./routes")
@ -31,6 +32,7 @@ router
.use("/health", ctx => (ctx.status = 200)) .use("/health", ctx => (ctx.status = 200))
.use("/version", ctx => (ctx.body = pkg.version)) .use("/version", ctx => (ctx.body = pkg.version))
.use(authenticated) .use(authenticated)
.use(currentApp)
// error handling middleware // error handling middleware
router.use(async (ctx, next) => { router.use(async (ctx, next) => {

View File

@ -1,52 +1,37 @@
const { const { getUserPermissions } = require("../utilities/security/roles")
BUILTIN_ROLE_IDS,
getUserPermissions,
} = require("../utilities/security/roles")
const { const {
PermissionTypes, PermissionTypes,
doesHaveResourcePermission, doesHaveResourcePermission,
doesHaveBasePermission, doesHaveBasePermission,
} = require("../utilities/security/permissions") } = require("../utilities/security/permissions")
const env = require("../environment")
const { isAPIKeyValid } = require("../utilities/security/apikey")
const { AuthTypes } = require("../constants")
const ADMIN_ROLES = [BUILTIN_ROLE_IDS.ADMIN, BUILTIN_ROLE_IDS.BUILDER]
function hasResource(ctx) { function hasResource(ctx) {
return ctx.resourceId != null return ctx.resourceId != null
} }
module.exports = (permType, permLevel = null) => async (ctx, next) => { const WEBHOOK_ENDPOINTS = new RegExp(
if (env.isProd() && ctx.headers["x-api-key"] && ctx.headers["x-instanceid"]) { ["webhooks/trigger", "webhooks/schema"].join("|")
// api key header passed by external webhook )
if (await isAPIKeyValid(ctx.headers["x-api-key"])) {
ctx.auth = {
authenticated: AuthTypes.EXTERNAL,
apiKey: ctx.headers["x-api-key"],
}
ctx.user = {
appId: ctx.headers["x-instanceid"],
}
return next()
}
return ctx.throw(403, "API key invalid") module.exports = (permType, permLevel = null) => async (ctx, next) => {
// webhooks don't need authentication, each webhook unique
if (WEBHOOK_ENDPOINTS.test(ctx.request.url)) {
return next()
} }
if (!ctx.user) { if (!ctx.user) {
return ctx.throw(403, "No user info found") return ctx.throw(403, "No user info found")
} }
const role = ctx.user.role
const isAdmin = ADMIN_ROLES.includes(role._id)
const isAuthed = ctx.isAuthenticated const isAuthed = ctx.isAuthenticated
const { basePermissions, permissions } = await getUserPermissions( const { basePermissions, permissions } = await getUserPermissions(
ctx.appId, ctx.appId,
role._id ctx.roleId
) )
// TODO: need to determine if the user has permission to build here, global cookie
// this may need to change in the future, right now only admins // this may need to change in the future, right now only admins
// can have access to builder features, this is hard coded into // can have access to builder features, this is hard coded into
// our rules // our rules

View File

@ -0,0 +1,57 @@
const { getAppId, setCookie, getCookie, Cookies } = require("@budibase/auth")
const { getGlobalUsers } = require("../utilities/workerRequests")
const { BUILTIN_ROLE_IDS } = require("../utilities/security/roles")
function CurrentAppCookie(appId, roleId) {
this.appId = appId
this.roleId = roleId
}
function finish(ctx, next, { appId, roleId, cookie = false }) {
if (appId) {
ctx.appId = appId
}
if (roleId) {
ctx.roleId = roleId
}
if (cookie && appId) {
setCookie(ctx, new CurrentAppCookie(appId, roleId))
}
return next()
}
module.exports = async (ctx, next) => {
// try to get the appID from the request
const requestAppId = getAppId(ctx)
// get app cookie if it exists
const appCookie = getCookie(ctx, Cookies.CurrentApp)
if (!appCookie && !requestAppId) {
return next()
}
let updateCookie = false,
appId,
roleId
if (!ctx.user) {
// not logged in, try to set a cookie for public apps
updateCookie = true
appId = requestAppId
roleId = BUILTIN_ROLE_IDS.PUBLIC
} else if (
requestAppId != null &&
(appCookie == null || requestAppId === appCookie.appId)
) {
const globalUser = await getGlobalUsers(ctx, requestAppId, ctx.user.email)
updateCookie = true
appId = requestAppId
roleId = globalUser.roles[requestAppId] || BUILTIN_ROLE_IDS.PUBLIC
} else if (requestAppId == null && appCookie != null) {
appId = appCookie.appId
roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC
}
return finish(ctx, next, {
appId: appId,
roleId: roleId,
cookie: updateCookie,
})
}

View File

@ -1,6 +1,5 @@
const authorizedMiddleware = require("../authorized") const authorizedMiddleware = require("../authorized")
const env = require("../../environment") const env = require("../../environment")
const apiKey = require("../../utilities/security/apikey")
const { AuthTypes } = require("../../constants") const { AuthTypes } = require("../../constants")
const { PermissionTypes, PermissionLevels } = require("../../utilities/security/permissions") const { PermissionTypes, PermissionLevels } = require("../../utilities/security/permissions")
jest.mock("../../environment", () => ({ jest.mock("../../environment", () => ({
@ -12,7 +11,6 @@ jest.mock("../../environment", () => ({
} }
}) })
) )
jest.mock("../../utilities/security/apikey")
class TestConfiguration { class TestConfiguration {
constructor(role) { constructor(role) {
@ -92,29 +90,6 @@ describe("Authorization middleware", () => {
"x-instanceid": "instance123", "x-instanceid": "instance123",
}) })
}) })
it("passes to next() if api key is valid", async () => {
apiKey.isAPIKeyValid.mockResolvedValueOnce(true)
await config.executeMiddleware()
expect(config.next).toHaveBeenCalled()
expect(config.ctx.auth).toEqual({
authenticated: AuthTypes.EXTERNAL,
apiKey: config.ctx.headers["x-api-key"],
})
expect(config.ctx.user).toEqual({
appId: config.ctx.headers["x-instanceid"],
})
})
it("throws if api key is invalid", async () => {
apiKey.isAPIKeyValid.mockResolvedValueOnce(false)
await config.executeMiddleware()
expect(config.throw).toHaveBeenCalledWith(403, "API key invalid")
})
}) })
describe("non-webhook call", () => { describe("non-webhook call", () => {

View File

@ -49,12 +49,11 @@ exports.getAppId = ctx => {
/** /**
* Get the name of the cookie which is to be updated/retrieved * 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 * @param {string} name The name/type of cookie.
* @returns {string} The name of the token trying to find * @returns {string} The full name of the cookie to retrieve/update.
*/ */
exports.getCookieName = (name = "builder") => { exports.getCookieName = name => {
let environment = env.isProd() ? "cloud" : "local" return `budibase:${name}`
return `budibase:${name}:${environment}`
} }
/** /**

View File

@ -1,19 +0,0 @@
const { apiKeyTable } = require("../../db/dynamoClient")
const env = require("../../environment")
/**
* This file purely exists so that we can centralise all logic pertaining to API keys, as their usage differs
* in our Cloud environment versus self hosted.
*/
exports.isAPIKeyValid = async apiKeyId => {
if (!env.SELF_HOSTED) {
let apiKeyInfo = await apiKeyTable.get({
primary: apiKeyId,
})
return apiKeyInfo != null
} else {
// if the api key supplied is correct then return structure similar
return apiKeyId === env.HOSTING_KEY
}
}