Merge pull request #8968 from Budibase/fix/user-self-update

Minor update - stop users updating their own admin/global flags
This commit is contained in:
Michael Drury 2022-12-08 09:01:41 +00:00 committed by GitHub
commit 45984e4c3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 83 additions and 26 deletions

View File

@ -1,9 +1,9 @@
import { ssoCallbackUrl } from "./utils"
import { authenticateThirdParty } from "./third-party-common"
import { authenticateThirdParty, SaveUserFunction } from "./third-party-common"
import { ConfigType, GoogleConfig, Database, SSOProfile } from "@budibase/types"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
export function buildVerifyFn(saveUserFn?: Function) {
export function buildVerifyFn(saveUserFn?: SaveUserFunction) {
return (
accessToken: string,
refreshToken: string,
@ -39,7 +39,7 @@ export function buildVerifyFn(saveUserFn?: Function) {
export async function strategyFactory(
config: GoogleConfig["config"],
callbackUrl: string,
saveUserFn?: Function
saveUserFn?: SaveUserFunction
) {
try {
const { clientID, clientSecret } = config

View File

@ -1,5 +1,5 @@
import fetch from "node-fetch"
import { authenticateThirdParty } from "./third-party-common"
import { authenticateThirdParty, SaveUserFunction } from "./third-party-common"
import { ssoCallbackUrl } from "./utils"
import {
Config,
@ -17,7 +17,7 @@ type JwtClaims = {
email: string
}
export function buildVerifyFn(saveUserFn?: Function) {
export function buildVerifyFn(saveUserFn?: SaveUserFunction) {
/**
* @param {*} issuer The identity provider base URL
* @param {*} sub The user ID
@ -106,7 +106,7 @@ function validEmail(value: string) {
*/
export async function strategyFactory(
config: OIDCConfiguration,
saveUserFn?: Function
saveUserFn?: SaveUserFunction
) {
try {
const verify = buildVerifyFn(saveUserFn)

View File

@ -9,6 +9,17 @@ import fetch from "node-fetch"
import { ThirdPartyUser } from "@budibase/types"
const jwt = require("jsonwebtoken")
type SaveUserOpts = {
requirePassword?: boolean
hashPassword?: boolean
currentUserId?: string
}
export type SaveUserFunction = (
user: ThirdPartyUser,
opts: SaveUserOpts
) => Promise<any>
/**
* Common authentication logic for third parties. e.g. OAuth, OIDC.
*/
@ -16,7 +27,7 @@ export async function authenticateThirdParty(
thirdPartyUser: ThirdPartyUser,
requireLocalAccount: boolean = true,
done: Function,
saveUserFn?: Function
saveUserFn?: SaveUserFunction
) {
if (!saveUserFn) {
throw new Error("Save user function must be provided")
@ -81,7 +92,7 @@ export async function authenticateThirdParty(
// create or sync the user
try {
await saveUserFn(dbUser, false, false)
await saveUserFn(dbUser, { hashPassword: false, requirePassword: false })
} catch (err: any) {
return authError(done, err)
}

View File

@ -51,6 +51,8 @@ export async function update(ctx: BBContext, next: any) {
}
// disallow updating your own role - always overwrite with DB roles
if (isLoggedInUser(ctx, user)) {
ctx.request.body.builder = user.builder
ctx.request.body.admin = user.admin
ctx.request.body.roles = user.roles
}
const response = await saveGlobalUser(publicApiUserFix(ctx))

View File

@ -13,6 +13,7 @@
},
"jest": {},
"devDependencies": {
"@types/json5": "^2.2.0",
"@types/koa": "2.13.4",
"@types/node": "14.18.20",
"@types/pouchdb": "6.4.0",

View File

@ -25,6 +25,7 @@ export interface ThirdPartyUser extends Document {
email: string
userId?: string
forceResetPassword?: boolean
userGroups?: string[]
}
export interface User extends ThirdPartyUser {
@ -42,7 +43,6 @@ export interface User extends ThirdPartyUser {
password?: string
status?: string
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
userGroups?: string[]
dayPassRecordedAt?: string
account?: {
authType: string

View File

@ -75,6 +75,13 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1"
integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==
"@types/json5@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-2.2.0.tgz#afff29abf9182a7d4a7e39105ca051f11c603d13"
integrity sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==
dependencies:
json5 "*"
"@types/keygrip@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
@ -447,6 +454,11 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
json5@*:
version "2.2.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"

View File

@ -24,7 +24,8 @@ const MAX_USERS_UPLOAD_LIMIT = 1000
export const save = async (ctx: any) => {
try {
ctx.body = await sdk.users.save(ctx.request.body)
const currentUserId = ctx.user._id
ctx.body = await sdk.users.save(ctx.request.body, { currentUserId })
} catch (err: any) {
ctx.throw(err.status || 400, err)
}

View File

@ -1,4 +1,4 @@
import { InviteUsersResponse } from "@budibase/types"
import { InviteUsersResponse, User } from "@budibase/types"
jest.mock("nodemailer")
import {
@ -298,6 +298,23 @@ describe("/api/global/users", () => {
expect(events.user.passwordForceReset).not.toBeCalled()
})
it("should not allow a user to update their own admin/builder status", async () => {
const user = (await config.api.users.getUser(config.defaultUser?._id!))
.body as User
await config.api.users.saveUser({
...user,
admin: {
global: false,
},
builder: {
global: false,
},
})
const userOut = (await config.api.users.getUser(user._id!)).body
expect(userOut.admin.global).toBe(true)
expect(userOut.builder.global).toBe(true)
})
it("should be able to force reset password", async () => {
const user = await config.createUser()
jest.clearAllMocks()

View File

@ -29,6 +29,7 @@ import {
RowResponse,
SearchUsersRequest,
User,
ThirdPartyUser,
} from "@budibase/types"
import { sendEmail } from "../../utilities/email"
import { EmailTemplatePurpose } from "../../constants"
@ -103,13 +104,14 @@ export const getUser = async (userId: string) => {
return user
}
interface SaveUserOpts {
export interface SaveUserOpts {
hashPassword?: boolean
requirePassword?: boolean
currentUserId?: string
}
const buildUser = async (
user: User,
user: User | ThirdPartyUser,
opts: SaveUserOpts = {
hashPassword: true,
requirePassword: true,
@ -117,7 +119,8 @@ const buildUser = async (
tenantId: string,
dbUser?: any
): Promise<User> => {
let { password, _id } = user
let fullUser = user as User
let { password, _id } = fullUser
let hashedPassword
if (password) {
@ -130,24 +133,24 @@ const buildUser = async (
_id = _id || dbUtils.generateGlobalUserID()
user = {
fullUser = {
createdAt: Date.now(),
...dbUser,
...user,
...fullUser,
_id,
password: hashedPassword,
tenantId,
}
// make sure the roles object is always present
if (!user.roles) {
user.roles = {}
if (!fullUser.roles) {
fullUser.roles = {}
}
// add the active status to a user if its not provided
if (user.status == null) {
user.status = constants.UserStatus.ACTIVE
if (fullUser.status == null) {
fullUser.status = constants.UserStatus.ACTIVE
}
return user
return fullUser
}
const validateUniqueUser = async (email: string, tenantId: string) => {
@ -169,12 +172,16 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
}
export const save = async (
user: User,
opts: SaveUserOpts = {
hashPassword: true,
requirePassword: true,
}
user: User | ThirdPartyUser,
opts: SaveUserOpts = {}
): Promise<CreateUserResponse> => {
// default booleans to true
if (opts.hashPassword == null) {
opts.hashPassword = true
}
if (opts.requirePassword == null) {
opts.requirePassword = true
}
const tenantId = tenancy.getTenantId()
const db = tenancy.getGlobalDB()
@ -213,6 +220,12 @@ export const save = async (
await validateUniqueUser(email, tenantId)
let builtUser = await buildUser(user, opts, tenantId, dbUser)
// don't allow a user to update its own roles/perms
if (opts.currentUserId && opts.currentUserId === dbUser?._id) {
builtUser.builder = dbUser.builder
builtUser.admin = dbUser.admin
builtUser.roles = dbUser.roles
}
// make sure we set the _id field for a new user
// Also if this is a new user, associate groups with them