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:
parent
0e583eb185
commit
eaad867780
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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: "/",
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue