Merge branch 'master' into fix/multi-tenancy-dev-access

This commit is contained in:
Michael Drury 2024-01-03 12:57:17 +00:00 committed by GitHub
commit e0a550e098
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 95 additions and 11 deletions

@ -1 +1 @@
Subproject commit c1a53bb2f4cafcb4c55ad7181146617b449907f2 Subproject commit aca3c9b6b5170d35a255ceb89e57a21719f5ed29

View File

@ -33,6 +33,7 @@ export * as docUpdates from "./docUpdates"
export * from "./utils/Duration" export * from "./utils/Duration"
export { SearchParams } from "./db" export { SearchParams } from "./db"
export * as docIds from "./docIds" export * as docIds from "./docIds"
export * as security from "./security"
// Add context to tenancy for backwards compatibility // Add context to tenancy for backwards compatibility
// only do this for external usages to prevent internal // only do this for external usages to prevent internal
// circular dependencies // circular dependencies

View File

@ -0,0 +1,24 @@
import { env } from ".."
export const PASSWORD_MIN_LENGTH = +(process.env.PASSWORD_MIN_LENGTH || 8)
export const PASSWORD_MAX_LENGTH = +(process.env.PASSWORD_MAX_LENGTH || 512)
export function validatePassword(
password: string
): { valid: true } | { valid: false; error: string } {
if (!password || password.length < PASSWORD_MIN_LENGTH) {
return {
valid: false,
error: `Password invalid. Minimum ${PASSWORD_MIN_LENGTH} characters.`,
}
}
if (password.length > PASSWORD_MAX_LENGTH) {
return {
valid: false,
error: `Password invalid. Maximum ${PASSWORD_MAX_LENGTH} characters.`,
}
}
return { valid: true }
}

View File

@ -0,0 +1 @@
export * from "./auth"

View File

@ -0,0 +1,45 @@
import { generator } from "../../../tests"
import { PASSWORD_MAX_LENGTH, validatePassword } from "../auth"
describe("auth", () => {
describe("validatePassword", () => {
it("a valid password returns successful", () => {
expect(validatePassword("password")).toEqual({ valid: true })
})
it.each([
["undefined", undefined],
["null", null],
["empty", ""],
])("%s returns unsuccessful", (_, password) => {
expect(validatePassword(password as string)).toEqual({
valid: false,
error: "Password invalid. Minimum 8 characters.",
})
})
it.each([
generator.word({ length: PASSWORD_MAX_LENGTH }),
generator.paragraph().substring(0, PASSWORD_MAX_LENGTH),
])(`can use passwords up to 512 characters in length`, password => {
expect(validatePassword(password)).toEqual({
valid: true,
})
})
it.each([
generator.word({ length: PASSWORD_MAX_LENGTH + 1 }),
generator
.paragraph({ sentences: 50 })
.substring(0, PASSWORD_MAX_LENGTH + 1),
])(
`passwords cannot have more than ${PASSWORD_MAX_LENGTH} characters`,
password => {
expect(validatePassword(password)).toEqual({
valid: false,
error: "Password invalid. Maximum 512 characters.",
})
}
)
})
})

View File

@ -27,6 +27,7 @@ import {
} from "./utils" } from "./utils"
import { searchExistingEmails } from "./lookup" import { searchExistingEmails } from "./lookup"
import { hash } from "../utils" import { hash } from "../utils"
import { validatePassword } from "../security"
type QuotaUpdateFn = ( type QuotaUpdateFn = (
change: number, change: number,
@ -110,6 +111,12 @@ export class UserDB {
if (await UserDB.isPreventPasswordActions(user, account)) { if (await UserDB.isPreventPasswordActions(user, account)) {
throw new HTTPError("Password change is disabled for this user", 400) throw new HTTPError("Password change is disabled for this user", 400)
} }
const passwordValidation = validatePassword(password)
if (!passwordValidation.valid) {
throw new HTTPError(passwordValidation.error, 400)
}
hashedPassword = opts.hashPassword ? await hash(password) : password hashedPassword = opts.hashPassword ? await hash(password) : password
} else if (dbUser) { } else if (dbUser) {
hashedPassword = dbUser.password hashedPassword = dbUser.password

View File

@ -21,7 +21,7 @@ export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
_id: userId, _id: userId,
userId, userId,
email: newEmail(), email: newEmail(),
password: "test", password: "password",
roles: { app_test: "admin" }, roles: { app_test: "admin" },
firstName: generator.first(), firstName: generator.first(),
lastName: generator.last(), lastName: generator.last(),

View File

@ -38,7 +38,7 @@
$goto("../portal") $goto("../portal")
} catch (error) { } catch (error) {
submitted = false submitted = false
notifications.error("Failed to create admin user") notifications.error(error.message || "Failed to create admin user")
} }
} }
</script> </script>

View File

@ -45,7 +45,7 @@
} }
} catch (err) { } catch (err) {
submitted = false submitted = false
notifications.error("Unable to reset password") notifications.error(err.message || "Unable to reset password")
} }
} }

View File

@ -48,6 +48,7 @@ async function init() {
HTTP_MIGRATIONS: "0", HTTP_MIGRATIONS: "0",
HTTP_LOGGING: "0", HTTP_LOGGING: "0",
VERSION: "0.0.0+local", VERSION: "0.0.0+local",
PASSWORD_MIN_LENGTH: "1",
} }
config = { ...config, ...existingConfig } config = { ...config, ...existingConfig }

View File

@ -30,6 +30,7 @@ async function init() {
ENABLE_EMAIL_TEST_MODE: "1", ENABLE_EMAIL_TEST_MODE: "1",
HTTP_LOGGING: "0", HTTP_LOGGING: "0",
VERSION: "0.0.0+local", VERSION: "0.0.0+local",
PASSWORD_MIN_LENGTH: "1",
} }
config = { ...config, ...existingConfig } config = { ...config, ...existingConfig }

View File

@ -122,10 +122,10 @@ export const resetUpdate = async (ctx: Ctx<PasswordResetUpdateRequest>) => {
ctx.body = { ctx.body = {
message: "password reset successfully.", message: "password reset successfully.",
} }
} catch (err) { } catch (err: any) {
console.warn(err) console.warn(err)
// hide any details of the error for security // hide any details of the error for security
ctx.throw(400, "Cannot reset password.") ctx.throw(400, err.message || "Cannot reset password.")
} }
} }

View File

@ -229,7 +229,7 @@ describe("/api/global/auth", () => {
) )
expect(res.body).toEqual({ expect(res.body).toEqual({
message: "Cannot reset password.", message: "Password change is disabled for this user",
status: 400, status: 400,
}) })
} }
@ -261,8 +261,12 @@ describe("/api/global/auth", () => {
) )
// convert to account owner now that password has been requested // convert to account owner now that password has been requested
const account = structures.accounts.ssoAccount() as CloudAccount const account: CloudAccount = {
mocks.accounts.getAccount.mockReturnValueOnce( ...structures.accounts.ssoAccount(),
budibaseUserId: "budibaseUserId",
email: user.email,
}
mocks.accounts.getAccountByTenantId.mockReturnValueOnce(
Promise.resolve(account) Promise.resolve(account)
) )

View File

@ -45,7 +45,7 @@ class TestConfiguration {
tenantId: string tenantId: string
user?: User user?: User
apiKey?: string apiKey?: string
userPassword = "test" userPassword = "password"
constructor(opts: { openServer: boolean } = { openServer: true }) { constructor(opts: { openServer: boolean } = { openServer: true }) {
// default to cloud hosting // default to cloud hosting

View File

@ -101,7 +101,7 @@ export class UserAPI extends TestAPI {
if (!request) { if (!request) {
request = { request = {
email: structures.email(), email: structures.email(),
password: generator.string(), password: generator.string({ length: 8 }),
tenantId: structures.tenant.id(), tenantId: structures.tenant.id(),
} }
} }