Merge pull request #8955 from Budibase/fix/7732

Dis-allow users updating their roles/deleting themselves through the public API
This commit is contained in:
Michael Drury 2022-12-07 10:53:32 +00:00 committed by GitHub
commit f92daa2d60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 94 additions and 29 deletions

View File

@ -5,9 +5,18 @@ import {
saveGlobalUser, saveGlobalUser,
} from "../../../utilities/workerRequests" } from "../../../utilities/workerRequests"
import { publicApiUserFix } from "../../../utilities/users" import { publicApiUserFix } from "../../../utilities/users"
import { db as dbCore } from "@budibase/backend-core"
import { search as stringSearch } from "./utils" import { search as stringSearch } from "./utils"
import { BBContext, User } from "@budibase/types"
function getUser(ctx: any, userId?: string) { function isLoggedInUser(ctx: BBContext, user: User) {
const loggedInId = ctx.user?._id
const globalUserId = dbCore.getGlobalIDFromUserMetadataID(loggedInId!)
// check both just incase
return globalUserId === user._id || loggedInId === user._id
}
function getUser(ctx: BBContext, userId?: string) {
if (userId) { if (userId) {
ctx.params = { userId } ctx.params = { userId }
} else if (!ctx.params?.userId) { } else if (!ctx.params?.userId) {
@ -16,37 +25,45 @@ function getUser(ctx: any, userId?: string) {
return readGlobalUser(ctx) return readGlobalUser(ctx)
} }
export async function search(ctx: any, next: any) { export async function search(ctx: BBContext, next: any) {
const { name } = ctx.request.body const { name } = ctx.request.body
const users = await allGlobalUsers(ctx) const users = await allGlobalUsers(ctx)
ctx.body = stringSearch(users, name, "email") ctx.body = stringSearch(users, name, "email")
await next() await next()
} }
export async function create(ctx: any, next: any) { export async function create(ctx: BBContext, next: any) {
const response = await saveGlobalUser(publicApiUserFix(ctx)) const response = await saveGlobalUser(publicApiUserFix(ctx))
ctx.body = await getUser(ctx, response._id) ctx.body = await getUser(ctx, response._id)
await next() await next()
} }
export async function read(ctx: any, next: any) { export async function read(ctx: BBContext, next: any) {
ctx.body = await readGlobalUser(ctx) ctx.body = await readGlobalUser(ctx)
await next() await next()
} }
export async function update(ctx: any, next: any) { export async function update(ctx: BBContext, next: any) {
const user = await readGlobalUser(ctx) const user = await readGlobalUser(ctx)
ctx.request.body = { ctx.request.body = {
...ctx.request.body, ...ctx.request.body,
_rev: user._rev, _rev: user._rev,
} }
// disallow updating your own role - always overwrite with DB roles
if (isLoggedInUser(ctx, user)) {
ctx.request.body.roles = user.roles
}
const response = await saveGlobalUser(publicApiUserFix(ctx)) const response = await saveGlobalUser(publicApiUserFix(ctx))
ctx.body = await getUser(ctx, response._id) ctx.body = await getUser(ctx, response._id)
await next() await next()
} }
export async function destroy(ctx: any, next: any) { export async function destroy(ctx: BBContext, next: any) {
const user = await getUser(ctx) const user = await getUser(ctx)
// disallow deleting yourself
if (isLoggedInUser(ctx, user)) {
ctx.throw(405, "Cannot delete user using its own API key.")
}
await deleteGlobalUser(ctx) await deleteGlobalUser(ctx)
ctx.body = user ctx.body = user
await next() await next()

View File

@ -1,41 +1,23 @@
const jestOpenAPI = require("jest-openapi").default const jestOpenAPI = require("jest-openapi").default
const generateSchema = require("../../../../../specs/generate") const generateSchema = require("../../../../../specs/generate")
const setup = require("../../tests/utilities") const setup = require("../../tests/utilities")
const { checkSlashesInUrl } = require("../../../../utilities") const { generateMakeRequest } = require("./utils")
const yamlPath = generateSchema() const yamlPath = generateSchema()
jestOpenAPI(yamlPath) jestOpenAPI(yamlPath)
let request = setup.getRequest()
let config = setup.getConfig() let config = setup.getConfig()
let apiKey, table, app let apiKey, table, app, makeRequest
beforeAll(async () => { beforeAll(async () => {
app = await config.init() app = await config.init()
table = await config.updateTable() table = await config.updateTable()
apiKey = await config.generateApiKey() apiKey = await config.generateApiKey()
makeRequest = generateMakeRequest(apiKey, setup)
}) })
afterAll(setup.afterAll) afterAll(setup.afterAll)
async function makeRequest(method, endpoint, body, appId = config.getAppId()) {
const extraHeaders = {
"x-budibase-api-key": apiKey,
}
if (appId) {
extraHeaders["x-budibase-app-id"] = appId
}
const req = request
[method](checkSlashesInUrl(`/api/public/v1/${endpoint}`))
.set(config.defaultHeaders(extraHeaders))
if (body) {
req.send(body)
}
const res = await req
expect(res.body).toBeDefined()
return res
}
describe("check the applications endpoints", () => { describe("check the applications endpoints", () => {
it("should allow retrieving applications through search", async () => { it("should allow retrieving applications through search", async () => {
const res = await makeRequest("post", "/applications/search") const res = await makeRequest("post", "/applications/search")

View File

@ -0,0 +1,38 @@
const setup = require("../../tests/utilities")
const { generateMakeRequest } = require("./utils")
const workerRequests = require("../../../../utilities/workerRequests")
let config = setup.getConfig()
let apiKey, globalUser, makeRequest
beforeAll(async () => {
await config.init()
globalUser = await config.globalUser()
apiKey = await config.generateApiKey(globalUser._id)
makeRequest = generateMakeRequest(apiKey, setup)
workerRequests.readGlobalUser.mockReturnValue(globalUser)
})
afterAll(setup.afterAll)
describe("check user endpoints", () => {
it("should not allow a user to update their own roles", async () => {
const res = await makeRequest("put", `/users/${globalUser._id}`, {
...globalUser,
roles: {
"app_1": "ADMIN",
}
})
expect(workerRequests.saveGlobalUser.mock.lastCall[0].body.data.roles["app_1"]).toBeUndefined()
expect(res.status).toBe(200)
expect(res.body.data.roles["app_1"]).toBeUndefined()
})
it("should not allow a user to delete themselves", async () => {
const res = await makeRequest("delete", `/users/${globalUser._id}`)
expect(res.status).toBe(405)
expect(workerRequests.deleteGlobalUser.mock.lastCall).toBeUndefined()
})
})

View File

@ -0,0 +1,28 @@
import { checkSlashesInUrl } from "../../../../utilities"
export function generateMakeRequest(apiKey: string, setup: any) {
const request = setup.getRequest()
const config = setup.getConfig()
return async (
method: string,
endpoint: string,
body?: any,
intAppId: string = config.getAppId()
) => {
const extraHeaders: any = {
"x-budibase-api-key": apiKey,
}
if (intAppId) {
extraHeaders["x-budibase-app-id"] = intAppId
}
const req = request[method](
checkSlashesInUrl(`/api/public/v1/${endpoint}`)
).set(config.defaultHeaders(extraHeaders))
if (body) {
req.send(body)
}
const res = await req
expect(res.body).toBeDefined()
return res
}
}

View File

@ -3,7 +3,7 @@ import env from "../environment"
import { checkSlashesInUrl } from "./index" import { checkSlashesInUrl } from "./index"
import { db as dbCore, constants, tenancy } from "@budibase/backend-core" import { db as dbCore, constants, tenancy } from "@budibase/backend-core"
import { updateAppRole } from "./global" import { updateAppRole } from "./global"
import { BBContext, Automation } from "@budibase/types" import { BBContext, User } from "@budibase/types"
export function request(ctx?: BBContext, request?: any) { export function request(ctx?: BBContext, request?: any) {
if (!request.headers) { if (!request.headers) {
@ -138,7 +138,7 @@ export async function deleteGlobalUser(ctx: BBContext) {
return checkResponse(response, "delete user", { ctx }) return checkResponse(response, "delete user", { ctx })
} }
export async function readGlobalUser(ctx: BBContext) { export async function readGlobalUser(ctx: BBContext): Promise<User> {
const response = await fetch( const response = await fetch(
checkSlashesInUrl( checkSlashesInUrl(
env.WORKER_URL + `/api/global/users/${ctx.params.userId}` env.WORKER_URL + `/api/global/users/${ctx.params.userId}`