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

View File

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

View File

@ -9,6 +9,17 @@ import fetch from "node-fetch"
import { ThirdPartyUser } from "@budibase/types" import { ThirdPartyUser } from "@budibase/types"
const jwt = require("jsonwebtoken") 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. * Common authentication logic for third parties. e.g. OAuth, OIDC.
*/ */
@ -16,7 +27,7 @@ export async function authenticateThirdParty(
thirdPartyUser: ThirdPartyUser, thirdPartyUser: ThirdPartyUser,
requireLocalAccount: boolean = true, requireLocalAccount: boolean = true,
done: Function, done: Function,
saveUserFn?: Function saveUserFn?: SaveUserFunction
) { ) {
if (!saveUserFn) { if (!saveUserFn) {
throw new Error("Save user function must be provided") throw new Error("Save user function must be provided")
@ -81,7 +92,7 @@ export async function authenticateThirdParty(
// create or sync the user // create or sync the user
try { try {
await saveUserFn(dbUser, false, false) await saveUserFn(dbUser, { hashPassword: false, requirePassword: false })
} catch (err: any) { } catch (err: any) {
return authError(done, err) 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 // disallow updating your own role - always overwrite with DB roles
if (isLoggedInUser(ctx, user)) { if (isLoggedInUser(ctx, user)) {
ctx.request.body.builder = user.builder
ctx.request.body.admin = user.admin
ctx.request.body.roles = user.roles ctx.request.body.roles = user.roles
} }
const response = await saveGlobalUser(publicApiUserFix(ctx)) const response = await saveGlobalUser(publicApiUserFix(ctx))

View File

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

View File

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

View File

@ -75,6 +75,13 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1"
integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w== 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@*": "@types/keygrip@*":
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" 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" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 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: mime-db@1.52.0:
version "1.52.0" version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 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) => { export const save = async (ctx: any) => {
try { 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) { } catch (err: any) {
ctx.throw(err.status || 400, err) 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") jest.mock("nodemailer")
import { import {
@ -298,6 +298,23 @@ describe("/api/global/users", () => {
expect(events.user.passwordForceReset).not.toBeCalled() 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 () => { it("should be able to force reset password", async () => {
const user = await config.createUser() const user = await config.createUser()
jest.clearAllMocks() jest.clearAllMocks()

View File

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