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 { SearchParams } from "./db"
export * as docIds from "./docIds"
export * as security from "./security"
// Add context to tenancy for backwards compatibility
// only do this for external usages to prevent internal
// 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"
import { searchExistingEmails } from "./lookup"
import { hash } from "../utils"
import { validatePassword } from "../security"
type QuotaUpdateFn = (
change: number,
@ -110,6 +111,12 @@ export class UserDB {
if (await UserDB.isPreventPasswordActions(user, account)) {
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
} else if (dbUser) {
hashedPassword = dbUser.password

View File

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

View File

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

View File

@ -45,7 +45,7 @@
}
} catch (err) {
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_LOGGING: "0",
VERSION: "0.0.0+local",
PASSWORD_MIN_LENGTH: "1",
}
config = { ...config, ...existingConfig }

View File

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

View File

@ -122,10 +122,10 @@ export const resetUpdate = async (ctx: Ctx<PasswordResetUpdateRequest>) => {
ctx.body = {
message: "password reset successfully.",
}
} catch (err) {
} catch (err: any) {
console.warn(err)
// 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({
message: "Cannot reset password.",
message: "Password change is disabled for this user",
status: 400,
})
}
@ -261,8 +261,12 @@ describe("/api/global/auth", () => {
)
// convert to account owner now that password has been requested
const account = structures.accounts.ssoAccount() as CloudAccount
mocks.accounts.getAccount.mockReturnValueOnce(
const account: CloudAccount = {
...structures.accounts.ssoAccount(),
budibaseUserId: "budibaseUserId",
email: user.email,
}
mocks.accounts.getAccountByTenantId.mockReturnValueOnce(
Promise.resolve(account)
)

View File

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

View File

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