Merge branch 'develop' into feature/migrations-2.0
This commit is contained in:
commit
54a32b41ef
|
@ -201,9 +201,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||||
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
||||||
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="#infra-Rory-Powell" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- markdownlint-restore -->
|
<!-- markdownlint-restore -->
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.46-alpha.7",
|
"version": "1.0.46-alpha.8",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.46-alpha.7",
|
"version": "1.0.46-alpha.8",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -12,6 +12,7 @@ const {
|
||||||
tenancy,
|
tenancy,
|
||||||
appTenancy,
|
appTenancy,
|
||||||
authError,
|
authError,
|
||||||
|
csrf,
|
||||||
internalApi,
|
internalApi,
|
||||||
} = require("./middleware")
|
} = require("./middleware")
|
||||||
|
|
||||||
|
@ -43,5 +44,6 @@ module.exports = {
|
||||||
buildAppTenancyMiddleware: appTenancy,
|
buildAppTenancyMiddleware: appTenancy,
|
||||||
auditLog,
|
auditLog,
|
||||||
authError,
|
authError,
|
||||||
|
buildCsrfMiddleware: csrf,
|
||||||
internalApi,
|
internalApi,
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ exports.Headers = {
|
||||||
TYPE: "x-budibase-type",
|
TYPE: "x-budibase-type",
|
||||||
TENANT_ID: "x-budibase-tenant-id",
|
TENANT_ID: "x-budibase-tenant-id",
|
||||||
TOKEN: "x-budibase-token",
|
TOKEN: "x-budibase-token",
|
||||||
|
CSRF_TOKEN: "x-csrf-token",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.GlobalRoles = {
|
exports.GlobalRoles = {
|
||||||
|
|
|
@ -60,6 +60,7 @@ module.exports = (
|
||||||
} else {
|
} else {
|
||||||
user = await getUser(userId, session.tenantId)
|
user = await getUser(userId, session.tenantId)
|
||||||
}
|
}
|
||||||
|
user.csrfToken = session.csrfToken
|
||||||
delete user.password
|
delete user.password
|
||||||
authenticated = true
|
authenticated = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
const { Headers } = require("../constants")
|
||||||
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET, HEAD and OPTIONS methods are considered safe operations
|
||||||
|
*
|
||||||
|
* POST, PUT, PATCH, and DELETE methods, being state changing verbs,
|
||||||
|
* should have a CSRF token attached to the request
|
||||||
|
*/
|
||||||
|
const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There are only three content type values that can be used in cross domain requests.
|
||||||
|
* If any other value is used, e.g. application/json, the browser will first make a OPTIONS
|
||||||
|
* request which will be protected by CORS.
|
||||||
|
*/
|
||||||
|
const INCLUDED_CONTENT_TYPES = [
|
||||||
|
"application/x-www-form-urlencoded",
|
||||||
|
"multipart/form-data",
|
||||||
|
"text/plain",
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the CSRF token generated aganst the user session.
|
||||||
|
* Compare the token with the x-csrf-token header.
|
||||||
|
*
|
||||||
|
* If the token is not found within the request or the value provided
|
||||||
|
* does not match the value within the user session, the request is rejected.
|
||||||
|
*
|
||||||
|
* CSRF protection provided using the 'Synchronizer Token Pattern'
|
||||||
|
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
module.exports = (opts = { noCsrfPatterns: [] }) => {
|
||||||
|
const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns)
|
||||||
|
return async (ctx, next) => {
|
||||||
|
// don't apply for excluded paths
|
||||||
|
const found = matches(ctx, noCsrfOptions)
|
||||||
|
if (found) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't apply for the excluded http methods
|
||||||
|
if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't apply when the content type isn't supported
|
||||||
|
let contentType = ctx.get("content-type")
|
||||||
|
? ctx.get("content-type").toLowerCase()
|
||||||
|
: ""
|
||||||
|
if (
|
||||||
|
!INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length
|
||||||
|
) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't apply csrf when the internal api key has been used
|
||||||
|
if (ctx.internal) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply csrf when there is a token in the session (new logins)
|
||||||
|
// in future there should be a hard requirement that the token is present
|
||||||
|
const userToken = ctx.user.csrfToken
|
||||||
|
if (!userToken) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reject if no token in request or mismatch
|
||||||
|
const requestToken = ctx.get(Headers.CSRF_TOKEN)
|
||||||
|
if (!requestToken || requestToken !== userToken) {
|
||||||
|
ctx.throw(403, "Invalid CSRF token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ const tenancy = require("./tenancy")
|
||||||
const appTenancy = require("./appTenancy")
|
const appTenancy = require("./appTenancy")
|
||||||
const internalApi = require("./internalApi")
|
const internalApi = require("./internalApi")
|
||||||
const datasourceGoogle = require("./passport/datasource/google")
|
const datasourceGoogle = require("./passport/datasource/google")
|
||||||
|
const csrf = require("./csrf")
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
google,
|
google,
|
||||||
|
@ -24,4 +25,5 @@ module.exports = {
|
||||||
datasource: {
|
datasource: {
|
||||||
google: datasourceGoogle,
|
google: datasourceGoogle,
|
||||||
},
|
},
|
||||||
|
csrf,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const redis = require("../redis/authRedis")
|
const redis = require("../redis/authRedis")
|
||||||
|
const { v4: uuidv4 } = require("uuid")
|
||||||
|
|
||||||
// a week in seconds
|
// a week in seconds
|
||||||
const EXPIRY_SECONDS = 86400 * 7
|
const EXPIRY_SECONDS = 86400 * 7
|
||||||
|
@ -16,6 +17,9 @@ function makeSessionID(userId, sessionId) {
|
||||||
exports.createASession = async (userId, session) => {
|
exports.createASession = async (userId, session) => {
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const sessionId = session.sessionId
|
const sessionId = session.sessionId
|
||||||
|
if (!session.csrfToken) {
|
||||||
|
session.csrfToken = uuidv4()
|
||||||
|
}
|
||||||
session = {
|
session = {
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
lastAccessedAt: new Date().toISOString(),
|
lastAccessedAt: new Date().toISOString(),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.46-alpha.7",
|
"version": "1.0.46-alpha.8",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.46-alpha.7",
|
"version": "1.0.46-alpha.8",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.46-alpha.7",
|
"@budibase/bbui": "^1.0.46-alpha.8",
|
||||||
"@budibase/client": "^1.0.46-alpha.7",
|
"@budibase/client": "^1.0.46-alpha.8",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/colorpicker": "1.1.2",
|
||||||
"@budibase/string-templates": "^1.0.46-alpha.7",
|
"@budibase/string-templates": "^1.0.46-alpha.8",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
import { store } from "./index"
|
import { store } from "./index"
|
||||||
import { get as svelteGet } from "svelte/store"
|
import { get as svelteGet } from "svelte/store"
|
||||||
import { removeCookie, Cookies } from "./cookies"
|
import { removeCookie, Cookies } from "./cookies"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
const apiCall =
|
const apiCall =
|
||||||
method =>
|
method =>
|
||||||
async (url, body, headers = { "Content-Type": "application/json" }) => {
|
async (url, body, headers = { "Content-Type": "application/json" }) => {
|
||||||
headers["x-budibase-app-id"] = svelteGet(store).appId
|
headers["x-budibase-app-id"] = svelteGet(store).appId
|
||||||
headers["x-budibase-api-version"] = "1"
|
headers["x-budibase-api-version"] = "1"
|
||||||
|
|
||||||
|
// add csrf token if authenticated
|
||||||
|
const user = svelteGet(auth).user
|
||||||
|
if (user && user.csrfToken) {
|
||||||
|
headers["x-csrf-token"] = user.csrfToken
|
||||||
|
}
|
||||||
|
|
||||||
const json = headers["Content-Type"] === "application/json"
|
const json = headers["Content-Type"] === "application/json"
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
await auth.checkAuth()
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.46-alpha.7",
|
"version": "1.0.46-alpha.8",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.46-alpha.7",
|
"version": "1.0.46-alpha.8",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.46-alpha.7",
|
"@budibase/bbui": "^1.0.46-alpha.8",
|
||||||
"@budibase/standard-components": "^0.9.139",
|
"@budibase/standard-components": "^0.9.139",
|
||||||
"@budibase/string-templates": "^1.0.46-alpha.7",
|
"@budibase/string-templates": "^1.0.46-alpha.8",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"rollup-plugin-polyfill-node": "^0.8.0",
|
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { notificationStore } from "stores"
|
import { notificationStore, authStore } from "stores"
|
||||||
|
import { get } from "svelte/store"
|
||||||
import { ApiVersion } from "constants"
|
import { ApiVersion } from "constants"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,6 +29,13 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
|
||||||
...(json && { "Content-Type": "application/json" }),
|
...(json && { "Content-Type": "application/json" }),
|
||||||
...(!inBuilder && { "x-budibase-type": "client" }),
|
...(!inBuilder && { "x-budibase-type": "client" }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add csrf token if authenticated
|
||||||
|
const auth = get(authStore)
|
||||||
|
if (auth && auth.csrfToken) {
|
||||||
|
headers["x-csrf-token"] = auth.csrfToken
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.46-alpha.7",
|
"version": "1.0.46-alpha.8",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -70,9 +70,9 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.0.3",
|
"@apidevtools/swagger-parser": "^10.0.3",
|
||||||
"@budibase/backend-core": "^1.0.46-alpha.7",
|
"@budibase/backend-core": "^1.0.46-alpha.8",
|
||||||
"@budibase/client": "^1.0.46-alpha.7",
|
"@budibase/client": "^1.0.46-alpha.8",
|
||||||
"@budibase/string-templates": "^1.0.46-alpha.7",
|
"@budibase/string-templates": "^1.0.46-alpha.8",
|
||||||
"@bull-board/api": "^3.7.0",
|
"@bull-board/api": "^3.7.0",
|
||||||
"@bull-board/koa": "^3.7.0",
|
"@bull-board/koa": "^3.7.0",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -16,6 +16,8 @@ exports.fetchSelf = async ctx => {
|
||||||
const user = await getFullUser(ctx, userId)
|
const user = await getFullUser(ctx, userId)
|
||||||
// this shouldn't be returned by the app self
|
// this shouldn't be returned by the app self
|
||||||
delete user.roles
|
delete user.roles
|
||||||
|
// forward the csrf token from the session
|
||||||
|
user.csrfToken = ctx.user.csrfToken
|
||||||
|
|
||||||
if (appId) {
|
if (appId) {
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
|
@ -24,6 +26,8 @@ exports.fetchSelf = async ctx => {
|
||||||
try {
|
try {
|
||||||
const userTable = await db.get(InternalTables.USER_METADATA)
|
const userTable = await db.get(InternalTables.USER_METADATA)
|
||||||
const metadata = await db.get(userId)
|
const metadata = await db.get(userId)
|
||||||
|
// make sure there is never a stale csrf token
|
||||||
|
delete metadata.csrfToken
|
||||||
// specifically needs to make sure is enriched
|
// specifically needs to make sure is enriched
|
||||||
ctx.body = await outputProcessing(ctx, userTable, {
|
ctx.body = await outputProcessing(ctx, userTable, {
|
||||||
...user,
|
...user,
|
||||||
|
|
|
@ -167,6 +167,8 @@ exports.updateSelfMetadata = async function (ctx) {
|
||||||
ctx.request.body._id = ctx.user._id
|
ctx.request.body._id = ctx.user._id
|
||||||
// make sure no stale rev
|
// make sure no stale rev
|
||||||
delete ctx.request.body._rev
|
delete ctx.request.body._rev
|
||||||
|
// make sure no csrf token
|
||||||
|
delete ctx.request.body.csrfToken
|
||||||
await exports.updateMetadata(ctx)
|
await exports.updateMetadata(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,9 @@ describe("/authenticate", () => {
|
||||||
|
|
||||||
describe("fetch self", () => {
|
describe("fetch self", () => {
|
||||||
it("should be able to fetch self", async () => {
|
it("should be able to fetch self", async () => {
|
||||||
const headers = await config.login()
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/self`)
|
.get(`/api/self`)
|
||||||
.set(headers)
|
.set(config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body._id).toEqual(generateUserMetadataID("us_uuid1"))
|
expect(res.body._id).toEqual(generateUserMetadataID("us_uuid1"))
|
||||||
|
|
|
@ -9,11 +9,59 @@ const {
|
||||||
} = require("@budibase/backend-core/permissions")
|
} = require("@budibase/backend-core/permissions")
|
||||||
const builderMiddleware = require("./builder")
|
const builderMiddleware = require("./builder")
|
||||||
const { isWebhookEndpoint } = require("./utils")
|
const { isWebhookEndpoint } = require("./utils")
|
||||||
|
const { buildCsrfMiddleware } = require("@budibase/backend-core/auth")
|
||||||
|
|
||||||
function hasResource(ctx) {
|
function hasResource(ctx) {
|
||||||
return ctx.resourceId != null
|
return ctx.resourceId != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const csrf = buildCsrfMiddleware()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply authorization to the requested resource:
|
||||||
|
* - If this is a builder resource the user must be a builder.
|
||||||
|
* - Builders can access all resources.
|
||||||
|
* - Otherwise the user must have the required role.
|
||||||
|
*/
|
||||||
|
const checkAuthorized = async (ctx, resourceRoles, permType, permLevel) => {
|
||||||
|
// check if this is a builder api and the user is not a builder
|
||||||
|
const isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
|
||||||
|
const isBuilderApi = permType === PermissionTypes.BUILDER
|
||||||
|
if (isBuilderApi && !isBuilder) {
|
||||||
|
return ctx.throw(403, "Not Authorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for resource authorization
|
||||||
|
if (!isBuilder) {
|
||||||
|
await checkAuthorizedResource(ctx, resourceRoles, permType, permLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkAuthorizedResource = async (
|
||||||
|
ctx,
|
||||||
|
resourceRoles,
|
||||||
|
permType,
|
||||||
|
permLevel
|
||||||
|
) => {
|
||||||
|
// get the user's roles
|
||||||
|
const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC
|
||||||
|
const userRoles = await getUserRoleHierarchy(ctx.appId, roleId, {
|
||||||
|
idOnly: false,
|
||||||
|
})
|
||||||
|
const permError = "User does not have permission"
|
||||||
|
// check if the user has the required role
|
||||||
|
if (resourceRoles.length > 0) {
|
||||||
|
// deny access if the user doesn't have the required resource role
|
||||||
|
const found = userRoles.find(role => resourceRoles.indexOf(role._id) !== -1)
|
||||||
|
if (!found) {
|
||||||
|
ctx.throw(403, permError)
|
||||||
|
}
|
||||||
|
// fallback to the base permissions when no resource roles are found
|
||||||
|
} else if (!doesHaveBasePermission(permType, permLevel, userRoles)) {
|
||||||
|
ctx.throw(403, permError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
(permType, permLevel = null) =>
|
(permType, permLevel = null) =>
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
|
@ -31,40 +79,26 @@ module.exports =
|
||||||
// to find API endpoints which are builder focused
|
// to find API endpoints which are builder focused
|
||||||
await builderMiddleware(ctx, permType)
|
await builderMiddleware(ctx, permType)
|
||||||
|
|
||||||
const isAuthed = ctx.isAuthenticated
|
// get the resource roles
|
||||||
// builders for now have permission to do anything
|
let resourceRoles = []
|
||||||
let isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
|
if (ctx.appId && hasResource(ctx)) {
|
||||||
const isBuilderApi = permType === PermissionTypes.BUILDER
|
resourceRoles = await getRequiredResourceRole(ctx.appId, permLevel, ctx)
|
||||||
if (isBuilder) {
|
|
||||||
return next()
|
|
||||||
} else if (isBuilderApi && !isBuilder) {
|
|
||||||
return ctx.throw(403, "Not Authorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to check this first, in-case public access, don't check authed until last
|
|
||||||
const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC
|
|
||||||
const hierarchy = await getUserRoleHierarchy(ctx.appId, roleId, {
|
|
||||||
idOnly: false,
|
|
||||||
})
|
|
||||||
const permError = "User does not have permission"
|
|
||||||
let possibleRoleIds = []
|
|
||||||
if (hasResource(ctx)) {
|
|
||||||
possibleRoleIds = await getRequiredResourceRole(ctx.appId, permLevel, ctx)
|
|
||||||
}
|
|
||||||
// check if we found a role, if not fallback to base permissions
|
|
||||||
if (possibleRoleIds.length > 0) {
|
|
||||||
const found = hierarchy.find(
|
|
||||||
role => possibleRoleIds.indexOf(role._id) !== -1
|
|
||||||
)
|
|
||||||
return found ? next() : ctx.throw(403, permError)
|
|
||||||
} else if (!doesHaveBasePermission(permType, permLevel, hierarchy)) {
|
|
||||||
ctx.throw(403, permError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if they are not authed, then anything using the authorized middleware will fail
|
|
||||||
if (!isAuthed) {
|
|
||||||
ctx.throw(403, "Session not authenticated")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the resource is public, proceed
|
||||||
|
const isPublicResource = resourceRoles.includes(BUILTIN_ROLE_IDS.PUBLIC)
|
||||||
|
if (isPublicResource) {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check authenticated
|
||||||
|
if (!ctx.isAuthenticated) {
|
||||||
|
return ctx.throw(403, "Session not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check authorized
|
||||||
|
await checkAuthorized(ctx, resourceRoles, permType, permLevel)
|
||||||
|
|
||||||
|
// csrf protection
|
||||||
|
return csrf(ctx, next)
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ class TestConfiguration {
|
||||||
this.middleware = authorizedMiddleware(role)
|
this.middleware = authorizedMiddleware(role)
|
||||||
this.next = jest.fn()
|
this.next = jest.fn()
|
||||||
this.throw = jest.fn()
|
this.throw = jest.fn()
|
||||||
|
this.headers = {}
|
||||||
this.ctx = {
|
this.ctx = {
|
||||||
headers: {},
|
headers: {},
|
||||||
request: {
|
request: {
|
||||||
|
@ -25,7 +26,8 @@ class TestConfiguration {
|
||||||
appId: "",
|
appId: "",
|
||||||
auth: {},
|
auth: {},
|
||||||
next: this.next,
|
next: this.next,
|
||||||
throw: this.throw
|
throw: this.throw,
|
||||||
|
get: (name) => this.headers[name],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +48,7 @@ class TestConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
setAuthenticated(isAuthed) {
|
setAuthenticated(isAuthed) {
|
||||||
this.ctx.auth = { authenticated: isAuthed }
|
this.ctx.isAuthenticated = isAuthed
|
||||||
}
|
}
|
||||||
|
|
||||||
setRequestUrl(url) {
|
setRequestUrl(url) {
|
||||||
|
@ -107,7 +109,7 @@ describe("Authorization middleware", () => {
|
||||||
expect(config.next).toHaveBeenCalled()
|
expect(config.next).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throws if the user has only builder permissions", async () => {
|
it("throws if the user does not have builder permissions", async () => {
|
||||||
config.setEnvironment(false)
|
config.setEnvironment(false)
|
||||||
config.setMiddlewareRequiredPermission(PermissionTypes.BUILDER)
|
config.setMiddlewareRequiredPermission(PermissionTypes.BUILDER)
|
||||||
config.setUser({
|
config.setUser({
|
||||||
|
@ -133,7 +135,7 @@ describe("Authorization middleware", () => {
|
||||||
expect(config.next).toHaveBeenCalled()
|
expect(config.next).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throws if the user session is not authenticated after permission checks", async () => {
|
it("throws if the user session is not authenticated", async () => {
|
||||||
config.setUser({
|
config.setUser({
|
||||||
role: {
|
role: {
|
||||||
_id: ""
|
_id: ""
|
||||||
|
|
|
@ -27,6 +27,7 @@ core.init(CouchDB)
|
||||||
|
|
||||||
const GLOBAL_USER_ID = "us_uuid1"
|
const GLOBAL_USER_ID = "us_uuid1"
|
||||||
const EMAIL = "babs@babs.com"
|
const EMAIL = "babs@babs.com"
|
||||||
|
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
||||||
|
|
||||||
class TestConfiguration {
|
class TestConfiguration {
|
||||||
constructor(openServer = true) {
|
constructor(openServer = true) {
|
||||||
|
@ -90,7 +91,11 @@ class TestConfiguration {
|
||||||
roles: roles || {},
|
roles: roles || {},
|
||||||
tenantId: TENANT_ID,
|
tenantId: TENANT_ID,
|
||||||
}
|
}
|
||||||
await createASession(id, { sessionId: "sessionid", tenantId: TENANT_ID })
|
await createASession(id, {
|
||||||
|
sessionId: "sessionid",
|
||||||
|
tenantId: TENANT_ID,
|
||||||
|
csrfToken: CSRF_TOKEN,
|
||||||
|
})
|
||||||
if (builder) {
|
if (builder) {
|
||||||
user.builder = { global: true }
|
user.builder = { global: true }
|
||||||
} else {
|
} else {
|
||||||
|
@ -137,6 +142,7 @@ class TestConfiguration {
|
||||||
`${Cookies.Auth}=${authToken}`,
|
`${Cookies.Auth}=${authToken}`,
|
||||||
`${Cookies.CurrentApp}=${appToken}`,
|
`${Cookies.CurrentApp}=${appToken}`,
|
||||||
],
|
],
|
||||||
|
[Headers.CSRF_TOKEN]: CSRF_TOKEN,
|
||||||
}
|
}
|
||||||
if (this.appId) {
|
if (this.appId) {
|
||||||
headers[Headers.APP_ID] = this.appId
|
headers[Headers.APP_ID] = this.appId
|
||||||
|
@ -430,10 +436,6 @@ class TestConfiguration {
|
||||||
roles: { [this.prodAppId]: roleId },
|
roles: { [this.prodAppId]: roleId },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await createASession(userId, {
|
|
||||||
sessionId: "sessionid",
|
|
||||||
tenantId: TENANT_ID,
|
|
||||||
})
|
|
||||||
// have to fake this
|
// have to fake this
|
||||||
const auth = {
|
const auth = {
|
||||||
userId,
|
userId,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/string-templates",
|
"name": "@budibase/string-templates",
|
||||||
"version": "1.0.46-alpha.7",
|
"version": "1.0.46-alpha.8",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.cjs",
|
"main": "src/index.cjs",
|
||||||
"module": "dist/bundle.mjs",
|
"module": "dist/bundle.mjs",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.46-alpha.7",
|
"version": "1.0.46-alpha.8",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "^1.0.46-alpha.7",
|
"@budibase/backend-core": "^1.0.46-alpha.8",
|
||||||
"@budibase/string-templates": "^1.0.46-alpha.7",
|
"@budibase/string-templates": "^1.0.46-alpha.8",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"@sentry/node": "^6.0.0",
|
"@sentry/node": "^6.0.0",
|
||||||
"@techpass/passport-openidconnect": "^0.3.0",
|
"@techpass/passport-openidconnect": "^0.3.0",
|
||||||
|
|
|
@ -172,6 +172,7 @@ exports.getSelf = async ctx => {
|
||||||
ctx.body.account = ctx.user.account
|
ctx.body.account = ctx.user.account
|
||||||
ctx.body.budibaseAccess = ctx.user.budibaseAccess
|
ctx.body.budibaseAccess = ctx.user.budibaseAccess
|
||||||
ctx.body.accountPortalAccess = ctx.user.accountPortalAccess
|
ctx.body.accountPortalAccess = ctx.user.accountPortalAccess
|
||||||
|
ctx.body.csrfToken = ctx.user.csrfToken
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateSelf = async ctx => {
|
exports.updateSelf = async ctx => {
|
||||||
|
@ -190,6 +191,8 @@ exports.updateSelf = async ctx => {
|
||||||
// don't allow sending up an ID/Rev, always use the existing one
|
// don't allow sending up an ID/Rev, always use the existing one
|
||||||
delete ctx.request.body._id
|
delete ctx.request.body._id
|
||||||
delete ctx.request.body._rev
|
delete ctx.request.body._rev
|
||||||
|
// don't allow setting the csrf token
|
||||||
|
delete ctx.request.body.csrfToken
|
||||||
const response = await db.put({
|
const response = await db.put({
|
||||||
...user,
|
...user,
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
|
|
|
@ -6,6 +6,7 @@ const {
|
||||||
buildAuthMiddleware,
|
buildAuthMiddleware,
|
||||||
auditLog,
|
auditLog,
|
||||||
buildTenancyMiddleware,
|
buildTenancyMiddleware,
|
||||||
|
buildCsrfMiddleware,
|
||||||
} = require("@budibase/backend-core/auth")
|
} = require("@budibase/backend-core/auth")
|
||||||
|
|
||||||
const PUBLIC_ENDPOINTS = [
|
const PUBLIC_ENDPOINTS = [
|
||||||
|
@ -68,6 +69,10 @@ const NO_TENANCY_ENDPOINTS = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// most public endpoints are gets, but some are posts
|
||||||
|
// add them all to be safe
|
||||||
|
const NO_CSRF_ENDPOINTS = [...PUBLIC_ENDPOINTS]
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
router
|
router
|
||||||
.use(
|
.use(
|
||||||
|
@ -85,6 +90,7 @@ router
|
||||||
.use("/health", ctx => (ctx.status = 200))
|
.use("/health", ctx => (ctx.status = 200))
|
||||||
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
||||||
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
|
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
|
||||||
|
.use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
|
||||||
// for now no public access is allowed to worker (bar health check)
|
// for now no public access is allowed to worker (bar health check)
|
||||||
.use((ctx, next) => {
|
.use((ctx, next) => {
|
||||||
if (ctx.publicEndpoint) {
|
if (ctx.publicEndpoint) {
|
||||||
|
|
|
@ -2,12 +2,12 @@ const env = require("../../../../environment")
|
||||||
const controllers = require("./controllers")
|
const controllers = require("./controllers")
|
||||||
const supertest = require("supertest")
|
const supertest = require("supertest")
|
||||||
const { jwt } = require("@budibase/backend-core/auth")
|
const { jwt } = require("@budibase/backend-core/auth")
|
||||||
const { Cookies } = require("@budibase/backend-core/constants")
|
const { Cookies, Headers } = require("@budibase/backend-core/constants")
|
||||||
const { Configs, LOGO_URL } = require("../../../../constants")
|
const { Configs, LOGO_URL } = require("../../../../constants")
|
||||||
const { getGlobalUserByEmail } = require("@budibase/backend-core/utils")
|
const { getGlobalUserByEmail } = require("@budibase/backend-core/utils")
|
||||||
const { createASession } = require("@budibase/backend-core/sessions")
|
const { createASession } = require("@budibase/backend-core/sessions")
|
||||||
const { newid } = require("@budibase/backend-core/src/hashing")
|
const { newid } = require("@budibase/backend-core/src/hashing")
|
||||||
const { TENANT_ID } = require("./structures")
|
const { TENANT_ID, CSRF_TOKEN } = require("./structures")
|
||||||
const core = require("@budibase/backend-core")
|
const core = require("@budibase/backend-core")
|
||||||
const CouchDB = require("../../../../db")
|
const CouchDB = require("../../../../db")
|
||||||
const { doInTenant } = require("@budibase/backend-core/tenancy")
|
const { doInTenant } = require("@budibase/backend-core/tenancy")
|
||||||
|
@ -72,6 +72,7 @@ class TestConfiguration {
|
||||||
await createASession("us_uuid1", {
|
await createASession("us_uuid1", {
|
||||||
sessionId: "sessionid",
|
sessionId: "sessionid",
|
||||||
tenantId: TENANT_ID,
|
tenantId: TENANT_ID,
|
||||||
|
csrfToken: CSRF_TOKEN,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +99,7 @@ class TestConfiguration {
|
||||||
return {
|
return {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
...this.cookieHeader([`${Cookies.Auth}=${authToken}`]),
|
...this.cookieHeader([`${Cookies.Auth}=${authToken}`]),
|
||||||
|
[Headers.CSRF_TOKEN]: CSRF_TOKEN,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
exports.TENANT_ID = "default"
|
exports.TENANT_ID = "default"
|
||||||
|
exports.CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
||||||
|
|
Loading…
Reference in New Issue