Increase minimum password length to 12 characters (#5712)

This commit is contained in:
gillespi314 2022-05-18 12:03:00 -05:00 committed by GitHub
parent c4962a2463
commit 4a4e832d3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 460 additions and 196 deletions

View file

@ -92,7 +92,7 @@ jobs:
make db-reset
./build/fleet serve --dev --dev_license 1>./fleet_log/stdout.log 2>./fleet_log/stderr.log &
./build/fleetctl config set --address http://localhost:1337 --tls-skip-verify
until ./build/fleetctl setup --email admin@example.com --name Admin --password admin123# --org-name Example
until ./build/fleetctl setup --email admin@example.com --name Admin --password preview1337# --org-name Example
do
echo "Retrying setup in 5s..."
sleep 5
@ -146,7 +146,7 @@ jobs:
timeout-minutes: 10
run: |
./build/fleetctl config set --address ${{ needs.gen.outputs.address }}
until ./build/fleetctl login --email admin@example.com --password admin123#
until ./build/fleetctl login --email admin@example.com --password preview1337#
do
echo "Retrying in 10s..."
sleep 10

View file

@ -128,7 +128,7 @@ jobs:
run: |
chmod +x ./build/fleetctl
./build/fleetctl config set --address ${{ needs.gen.outputs.address }}
until ./build/fleetctl login --email admin@example.com --password admin123#
until ./build/fleetctl login --email admin@example.com --password preview1337#
do
echo "Retrying in 5s..."
sleep 5

View file

@ -246,9 +246,9 @@ e2e-reset-db:
e2e-setup:
./build/fleetctl config set --context e2e --address https://localhost:8642 --tls-skip-verify true
./build/fleetctl setup --context e2e --email=admin@example.com --password=user123# --org-name='Fleet Test' --name Admin
./build/fleetctl user create --context e2e --email=maintainer@example.com --name maintainer --password=user123# --global-role=maintainer
./build/fleetctl user create --context e2e --email=observer@example.com --name observer --password=user123# --global-role=observer
./build/fleetctl setup --context e2e --email=admin@example.com --password=password123# --org-name='Fleet Test' --name Admin
./build/fleetctl user create --context e2e --email=maintainer@example.com --name maintainer --password=password123# --global-role=maintainer
./build/fleetctl user create --context e2e --email=observer@example.com --name observer --password=password123# --global-role=observer
./build/fleetctl user create --context e2e --email=sso_user@example.com --name "SSO user" --sso=true
e2e-serve-free: e2e-reset-db

View file

@ -0,0 +1 @@
* Increase minimum password length to 12 characters

View file

@ -222,7 +222,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
const (
address = "https://localhost:8412"
email = "admin@example.com"
password = "admin123#"
password = "preview1337#"
)
fleetClient, err := service.NewClient(address, true, "", "")

View file

@ -153,6 +153,8 @@ func createUserCommand() *cli.Command {
// Only set the password reset flag if SSO is not enabled and user is not API-only. Otherwise
// the user will be stuck in a bad state and not be able to log in.
force_reset := !sso && !apiOnly
// password requirements are validated as part of `CreateUser`
err = client.CreateUser(fleet.UserPayload{
Password: &password,
Email: &email,

View file

@ -5,6 +5,8 @@ import (
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -49,6 +51,8 @@ func (e *notFoundError) Error() string {
func TestUserCreateForcePasswordReset(t *testing.T) {
_, ds := runServerWithMockedDS(t)
pwd := test.GoodPassword
ds.InviteByEmailFunc = func(ctx context.Context, email string) (*fleet.Invite, error) {
return nil, &notFoundError{}
}
@ -65,7 +69,7 @@ func TestUserCreateForcePasswordReset(t *testing.T) {
},
{
name: "api-only",
args: []string{"--email", "bar@example.com", "--password", "p4ssw0rd.", "--name", "bar", "--api-only"},
args: []string{"--email", "bar@example.com", "--password", pwd, "--name", "bar", "--api-only"},
expectedAdminForcePasswordReset: false,
},
{
@ -75,7 +79,7 @@ func TestUserCreateForcePasswordReset(t *testing.T) {
},
{
name: "non-sso-non-api-only",
args: []string{"--email", "zoo@example.com", "--password", "p4ssw0rd.", "--name", "zoo"},
args: []string{"--email", "zoo@example.com", "--password", pwd, "--name", "zoo"},
expectedAdminForcePasswordReset: true,
},
} {

View file

@ -1,3 +1,12 @@
import CONSTANTS from "../../../support/constants";
const {
GOOD_PASSWORD,
BAD_PASSWORD_LENGTH,
BAD_PASSWORD_NO_NUMBER,
BAD_PASSWORD_NO_SYMBOL,
} = CONSTANTS;
describe("Activate user flow", () => {
before(() => {
Cypress.session.clearAllSavedSessions();
@ -35,7 +44,7 @@ describe("Activate user flow", () => {
cy.wait(3000); // eslint-disable-line cypress/no-unnecessary-waiting
cy.logout();
// Retrieves user invite in email
const inviteLink = {};
const inviteLink = { url: "" };
const regex = /\/login\/invites\/[a-zA-Z0-9=?%&@._-]*/gm;
cy.getEmails().then((response) => {
expect(response.body.items[0].To[0]).to.have.property("Domain");
@ -54,17 +63,40 @@ describe("Activate user flow", () => {
.type("Ash Ketchum");
cy.findByLabelText(/^password$/i)
.click()
.type("#pikachu1");
.type(BAD_PASSWORD_LENGTH);
cy.findByLabelText(/confirm password/i)
.click()
.type("#pikachu1");
.type(BAD_PASSWORD_LENGTH);
cy.findByRole("button", { name: /submit/i }).click();
});
cy.findByText(/password does not meet required criteria/i).should(
"exist"
);
cy.getAttached("#password").clear().type(BAD_PASSWORD_NO_NUMBER);
cy.getAttached("#password_confirmation")
.clear()
.type(BAD_PASSWORD_NO_NUMBER);
cy.findByRole("button", { name: /submit/i }).click();
cy.findByText(/password does not meet required criteria/i).should(
"exist"
);
cy.getAttached("#password").clear().type(BAD_PASSWORD_NO_SYMBOL);
cy.getAttached("#password_confirmation")
.clear()
.type(BAD_PASSWORD_NO_SYMBOL);
cy.findByRole("button", { name: /submit/i }).click();
cy.findByText(/password does not meet required criteria/i).should(
"exist"
);
cy.getAttached("#password").clear().type(GOOD_PASSWORD);
cy.getAttached("#password_confirmation").clear().type(GOOD_PASSWORD);
cy.findByRole("button", { name: /submit/i }).click();
cy.getAttached(".login-form").within(() => {
cy.findByLabelText(/email/i).clear().type("ash@example.com");
cy.findByLabelText(/^password$/i)
.click()
.type("#pikachu1");
.type(GOOD_PASSWORD);
cy.findByRole("button", { name: /login/i }).click();
});
Cypress.session.clearAllSavedSessions(); // Switch back to admin user

View file

@ -1,3 +1,12 @@
import CONSTANTS from "../,,/../../../support/constants";
const {
GOOD_PASSWORD,
BAD_PASSWORD_LENGTH,
BAD_PASSWORD_NO_NUMBER,
BAD_PASSWORD_NO_SYMBOL,
} = CONSTANTS;
describe("Manage users flow", () => {
before(() => {
Cypress.session.clearAllSavedSessions();
@ -32,7 +41,6 @@ describe("Manage users flow", () => {
cy.contains("button:enabled", /create user/i).click();
cy.findByPlaceholderText("Full name").type("New Name");
cy.findByPlaceholderText("Email").type("new-user@example.com");
cy.findByPlaceholderText("Password").type("user123#");
cy.getAttached(
".create-user-form__form-field--global-role > .Select"
).click();
@ -41,6 +49,28 @@ describe("Manage users flow", () => {
cy.findByText(/maintainer/i).click();
}
);
cy.findByPlaceholderText("Password").clear().type(BAD_PASSWORD_LENGTH);
cy.getAttached(".create-user-form__btn-wrap")
.contains("button", /create/i)
.click();
cy.findByText(/password must meet the criteria below/i).should("exist");
cy.findByLabelText(/password must meet the criteria below/i)
.clear()
.type(BAD_PASSWORD_NO_NUMBER);
cy.getAttached(".create-user-form__btn-wrap")
.contains("button", /create/i)
.click();
cy.findByText(/password must meet the criteria below/i).should("exist");
cy.findByLabelText(/password must meet the criteria below/i)
.clear()
.type(BAD_PASSWORD_NO_NUMBER);
cy.getAttached(".create-user-form__btn-wrap")
.contains("button", /create/i)
.click();
cy.findByText(/password must meet the criteria below/i).should("exist");
cy.findByLabelText(/password must meet the criteria below/i)
.clear()
.type(GOOD_PASSWORD);
cy.getAttached(".create-user-form__btn-wrap")
.contains("button", /create/i)
.click();
@ -64,6 +94,28 @@ describe("Manage users flow", () => {
cy.findByText(/admin/i).click();
}
);
cy.findByLabelText("Password").clear().type(BAD_PASSWORD_LENGTH);
cy.getAttached(".create-user-form__btn-wrap")
.contains("button", /save/i)
.click();
cy.findByText(/password must meet the criteria below/i).should("exist");
cy.findByLabelText(/password must meet the criteria below/i)
.clear()
.type(BAD_PASSWORD_NO_NUMBER);
cy.getAttached(".create-user-form__btn-wrap")
.contains("button", /save/i)
.click();
cy.findByText(/password must meet the criteria below/i).should("exist");
cy.findByLabelText(/password must meet the criteria below/i)
.clear()
.type(BAD_PASSWORD_NO_SYMBOL);
cy.getAttached(".create-user-form__btn-wrap")
.contains("button", /save/i)
.click();
cy.findByText(/password must meet the criteria below/i).should("exist");
cy.findByLabelText(/password must meet the criteria below/i)
.clear()
.type(GOOD_PASSWORD);
cy.getAttached(".create-user-form__btn-wrap")
.contains("button", /save/i)
.click();

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
describe("Sessions", () => {
before(() => {
Cypress.session.clearAllSavedSessions();
@ -8,7 +12,7 @@ describe("Sessions", () => {
cy.getAttached(".login-form__forgot-link").should("exist");
// Log in
cy.getAttached("input").first().type("admin@example.com");
cy.getAttached("input").last().type("user123#");
cy.getAttached("input").last().type(GOOD_PASSWORD);
cy.getAttached("button").click();
// Verify dashboard
cy.url().should("include", "/dashboard");

View file

@ -1,3 +1,9 @@
import CONSTANTS from "../../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
const enable_idp_login = true;
describe("SSO Sessions", () => {
beforeEach(() => {
Cypress.session.clearAllSavedSessions();
@ -5,13 +11,13 @@ describe("SSO Sessions", () => {
});
it("non-SSO user can login with username/password", () => {
cy.login();
cy.setupSSO((enable_idp_login = true));
cy.setupSSO(enable_idp_login);
cy.logout();
cy.visit("/");
cy.getAttached(".login-form__forgot-link").should("exist");
// Log in
cy.getAttached("input").first().type("admin@example.com");
cy.getAttached("input").last().type("user123#");
cy.getAttached("input").last().type(GOOD_PASSWORD);
cy.contains("button", "Login").click();
// Verify dashboard
cy.url().should("include", "/dashboard");
@ -23,7 +29,7 @@ describe("SSO Sessions", () => {
});
it("can login via SSO", () => {
cy.login();
cy.setupSSO((enable_idp_login = true));
cy.setupSSO(enable_idp_login);
cy.logout();
cy.visit("/");
// Log in

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
describe("Setup", () => {
// Different than normal beforeEach because we don't run the fleetctl setup.
beforeEach(() => {
@ -20,11 +24,11 @@ describe("Setup", () => {
cy.findByPlaceholderText(/^password/i)
.first()
.type("admin123#");
.type(GOOD_PASSWORD);
cy.findByPlaceholderText(/confirm password/i)
.last()
.type("admin123#");
.type(GOOD_PASSWORD);
cy.contains("button:enabled", /next/i).click();

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
const getConfig = {
org_info: {
org_name: "Fleet Test",
@ -286,7 +290,7 @@ describe(
});
describe("Navigation", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.visit("/dashboard");
});
it("displays intended admin top navigation", () => {
@ -313,7 +317,7 @@ describe(
});
describe("Dashboard", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.visit("/dashboard");
});
it("displays cards for all platforms", () => {
@ -412,7 +416,7 @@ describe(
});
describe("Manage hosts page", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.visit("/hosts/manage");
});
it("verifies teams is disabled on Manage Host page", () => {
@ -435,7 +439,7 @@ describe(
});
describe("Host details tests", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.visit("/hosts/1");
});
it("verifies teams is disabled on Host Details page", () => {
@ -459,7 +463,7 @@ describe(
});
describe("Manage software page", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.intercept("GET", "/api/latest/fleet/config", getConfig).as(
"getIntegrations"
);
@ -586,7 +590,7 @@ describe(
});
describe("Query pages", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.visit("/queries/manage");
});
it("allows admin add a new query", () => {
@ -637,7 +641,7 @@ describe(
});
describe("Manage policies page", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.visit("/policies/manage");
});
it("allows admin to click 'Manage automations' button", () => {
@ -683,7 +687,7 @@ describe(
return false;
});
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.visit("/settings/users");
});
it("hides access team settings", () => {
@ -727,7 +731,7 @@ describe(
});
describe("User profile page", () => {
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
cy.visit("/profile");
});
it("verifies teams is disabled for the Profile page", () => {

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
describe(
"Free tier - Maintainer user",
{
@ -21,7 +25,7 @@ describe(
describe("Navigation", () => {
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
cy.visit("/dashboard");
});
it("displays intended global maintainer top navigation", () => {
@ -39,7 +43,7 @@ describe(
});
describe("Dashboard", () => {
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
cy.visit("/dashboard");
});
it("displays cards for all platforms", () => {
@ -138,7 +142,7 @@ describe(
});
describe("Manage hosts page", () => {
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
cy.visit("/hosts/manage");
});
it("verifies maintainer is on the Manage Hosts page", () => {
@ -164,7 +168,7 @@ describe(
});
describe("Host details tests", () => {
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
cy.visit("/hosts/1");
});
it("verifies teams is disabled", () => {
@ -194,7 +198,7 @@ describe(
});
describe("Query pages", () => {
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
cy.visit("/queries/manage");
});
it("allows maintainer to add a new query", () => {
@ -247,7 +251,7 @@ describe(
});
describe("Manage policies page", () => {
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
cy.visit("/policies/manage");
});
it("hides manage automations from maintainer", () => {
@ -299,7 +303,7 @@ describe(
});
describe("Manage packs page", () => {
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
cy.visit("/packs/manage");
});
it("allows maintainer to create a pack", () => {
@ -329,7 +333,7 @@ describe(
});
describe("User profile page", () => {
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
cy.visit("/profile");
});
it("verifies teams is disabled for the Profile page", () => {
@ -356,7 +360,7 @@ describe(
return false;
});
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
});
it("verifies maintainer does not have access to settings", () => {
cy.findByText(/settings/i).should("not.exist");

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
describe("Free tier - Observer user", () => {
before(() => {
Cypress.session.clearAllSavedSessions();
@ -16,7 +20,7 @@ describe("Free tier - Observer user", () => {
describe("Navigation", () => {
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
cy.visit("/dashboard");
});
it("displays intended global observer top navigation", () => {
@ -34,7 +38,7 @@ describe("Free tier - Observer user", () => {
});
describe("Dashboard", () => {
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
cy.visit("/dashboard");
});
it("displays cards for all platforms", () => {
@ -133,7 +137,7 @@ describe("Free tier - Observer user", () => {
});
describe("Manage hosts page", () => {
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
cy.visit("/hosts/manage");
});
it("verifies teams is disabled on Manage Host page", () => {
@ -151,7 +155,7 @@ describe("Free tier - Observer user", () => {
});
describe("Host details page", () => {
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
cy.visit("/hosts/1");
});
it("verifies teams is disabled on Host Details page", () => {
@ -169,7 +173,7 @@ describe("Free tier - Observer user", () => {
});
describe("Manage software page", () => {
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
cy.visit("/software/manage");
});
it("hides manage automations button", () => {
@ -182,7 +186,7 @@ describe("Free tier - Observer user", () => {
});
describe("Query page", () => {
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
cy.visit("/queries/manage");
});
it("hides create a query button", () => {
@ -205,7 +209,7 @@ describe("Free tier - Observer user", () => {
});
describe("Manage policies page", () => {
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
cy.visit("/policies/manage");
});
it("hides manage automations button", () => {
@ -235,7 +239,7 @@ describe("Free tier - Observer user", () => {
});
describe("User profile page", () => {
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
cy.visit("/profile");
});
it("verifies teams is disabled for the Profile page", () => {
@ -262,7 +266,7 @@ describe("Free tier - Observer user", () => {
return false;
});
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
});
it("should restrict navigation according to role-based access controls", () => {
cy.findByText(/settings/i).should("not.exist");

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
const getConfig = {
org_info: {
org_name: "Fleet Test",
@ -281,7 +285,7 @@ describe("Premium tier - Global Admin user", () => {
});
beforeEach(() => {
cy.loginWithCySession("anna@organization.com", "user123#");
cy.loginWithCySession("anna@organization.com", GOOD_PASSWORD);
});
describe("Navigation", () => {
beforeEach(() => cy.visit("/dashboard"));

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
describe("Premium tier - Maintainer user", () => {
before(() => {
Cypress.session.clearAllSavedSessions();
@ -15,7 +19,7 @@ describe("Premium tier - Maintainer user", () => {
describe("Global maintainer", () => {
beforeEach(() => {
cy.loginWithCySession("mary@organization.com", "user123#");
cy.loginWithCySession("mary@organization.com", GOOD_PASSWORD);
});
describe("Navigation", () => {
beforeEach(() => cy.visit("/dashboard"));

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
describe("Premium tier - Observer user", () => {
before(() => {
Cypress.session.clearAllSavedSessions();
@ -16,7 +20,7 @@ describe("Premium tier - Observer user", () => {
describe("Global observer", () => {
beforeEach(() => {
cy.loginWithCySession("oliver@organization.com", "user123#");
cy.loginWithCySession("oliver@organization.com", GOOD_PASSWORD);
});
describe("Navigation", () => {
beforeEach(() => cy.visit("/dashboard"));
@ -241,7 +245,7 @@ describe("Premium tier - Observer user", () => {
describe("Team observer", () => {
beforeEach(() => {
cy.loginWithCySession("toni@organization.com", "user123#");
cy.loginWithCySession("toni@organization.com", GOOD_PASSWORD);
});
describe("Nav restrictions", () => {
it("should restrict navigation according to role-based access controls", () => {

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
describe("Premium tier - Team Admin user", () => {
before(() => {
Cypress.session.clearAllSavedSessions();
@ -15,7 +19,7 @@ describe("Premium tier - Team Admin user", () => {
});
beforeEach(() => {
cy.loginWithCySession("anita@organization.com", "user123#");
cy.loginWithCySession("anita@organization.com", GOOD_PASSWORD);
});
describe("Navigation", () => {
beforeEach(() => cy.visit("/dashboard"));

View file

@ -1,3 +1,7 @@
import CONSTANTS from "../../support/constants";
const { GOOD_PASSWORD } = CONSTANTS;
describe("Premium tier - Team observer/maintainer user", () => {
before(() => {
Cypress.session.clearAllSavedSessions();
@ -15,7 +19,7 @@ describe("Premium tier - Team observer/maintainer user", () => {
});
describe("Team maintainer and team observer", () => {
beforeEach(() => {
cy.loginWithCySession("marco@organization.com", "user123#");
cy.loginWithCySession("marco@organization.com", GOOD_PASSWORD);
});
describe("Navigation", () => {
beforeEach(() => cy.visit("/dashboard"));
@ -132,7 +136,7 @@ describe("Premium tier - Team observer/maintainer user", () => {
});
describe("Team observer", () => {
beforeEach(() => {
cy.loginWithCySession("marco@organization.com", "user123#");
cy.loginWithCySession("marco@organization.com", GOOD_PASSWORD);
});
describe("Manage hosts page", () => {
it("should render elements according to role-based access controls", () => {
@ -200,7 +204,7 @@ describe("Premium tier - Team observer/maintainer user", () => {
});
beforeEach(() => {
cy.loginWithCySession("marco@organization.com", "user123#");
cy.loginWithCySession("marco@organization.com", GOOD_PASSWORD);
cy.visit("/hosts/manage");
});
describe("Manage hosts page", () => {

View file

@ -1,5 +1,8 @@
import "@testing-library/cypress/add-commands";
import "cypress-wait-until";
import CONSTANTS from "./constants";
const { GOOD_PASSWORD } = CONSTANTS;
// ***********************************************
// This example commands.js shows you how to
@ -38,7 +41,7 @@ Cypress.Commands.add("setup", () => {
Cypress.Commands.add("login", (email, password) => {
email ||= "admin@example.com";
password ||= "user123#";
password ||= GOOD_PASSWORD;
cy.request("POST", "/api/latest/fleet/login", { email, password }).then(
(resp) => {
window.localStorage.setItem("FLEET::auth_token", resp.body.token);
@ -48,7 +51,7 @@ Cypress.Commands.add("login", (email, password) => {
Cypress.Commands.add("loginWithCySession", (email, password) => {
email ||= "admin@example.com";
password ||= "user123#";
password ||= GOOD_PASSWORD;
cy.session([email, password], () => {
cy.request("POST", "/api/latest/fleet/login", { email, password }).then(
(resp) => {
@ -154,6 +157,7 @@ Cypress.Commands.add("seedSchedule", () => {
});
});
// @ts-ignore
Cypress.Commands.add("seedPacks", () => {
const packs = [
{
@ -377,7 +381,7 @@ Cypress.Commands.add("seedFigma", () => {
Cypress.Commands.add("addUser", (options = {}) => {
let { password, email, globalRole } = options;
password ||= "test123#";
password ||= GOOD_PASSWORD;
email ||= `admin@example.com`;
globalRole ||= "admin";

View file

@ -0,0 +1,11 @@
const GOOD_PASSWORD = "password123#";
const BAD_PASSWORD_LENGTH = "password12#";
const BAD_PASSWORD_NO_NUMBER = "password####";
const BAD_PASSWORD_NO_SYMBOL = "password1234";
export default {
GOOD_PASSWORD,
BAD_PASSWORD_LENGTH,
BAD_PASSWORD_NO_NUMBER,
BAD_PASSWORD_NO_SYMBOL,
};

View file

@ -37,7 +37,7 @@ declare namespace Cypress {
/**
* Custom command to add new policies by default.
*/
seedPolicies(): Chainable<Element>;
seedPolicies(teamName?: string): Chainable<Element>;
/**
* Custom command to add a new user in Fleet (via fleetctl).
@ -72,7 +72,7 @@ declare namespace Cypress {
/**
* Custom command to get the emails handled by the Mailhog server.
*/
getEmails(): Chainable<Response>;
getEmails(): Chainable;
/**
* Custom command to seed the Free tier teams/users.
@ -104,7 +104,7 @@ declare namespace Cypress {
* NOTE: login() command is required before this, as it will make authenticated
* requests.
*/
addDockerHost(): Chainable;
addDockerHost(teamName?: string): Chainable;
/**
* Custom command to stop any running Docker hosts.

View file

@ -50,7 +50,7 @@ text or HTML as mentioned before.
value={password || ""}
type="password"
hint={[
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
blockAutoComplete
tooltip={`\

View file

@ -41,7 +41,7 @@ class ChangePasswordForm extends Component {
label="New password"
type="password"
hint={[
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
/>
<InputField

View file

@ -123,9 +123,9 @@ describe("ChangePasswordForm - component", () => {
render(<ChangePasswordForm {...props} />);
const expectedFormData = {
old_password: "p@ssw0rd",
new_password: "p@ssw0rd1",
new_password_confirmation: "p@ssw0rd1",
old_password: "password123#",
new_password: "password123!",
new_password_confirmation: "password123!",
};
// when

View file

@ -42,7 +42,7 @@ class ConfirmInviteForm extends Component {
placeholder="Password"
type="password"
hint={[
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
/>
<InputFieldWithIcon

View file

@ -67,7 +67,7 @@ class AdminDetails extends Component {
type="password"
tabIndex={tabIndex}
hint={[
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
/>
<InputFieldWithIcon

View file

@ -83,8 +83,11 @@ describe("AdminDetails - form", () => {
screen.getByRole("textbox", { name: "Email" }),
"hi@gnar.dog"
);
userEvent.type(screen.getByPlaceholderText("Password"), "p@ssw0rd");
userEvent.type(screen.getByPlaceholderText("Confirm password"), "p@ssw0rd");
userEvent.type(screen.getByPlaceholderText("Password"), "password123#");
userEvent.type(
screen.getByPlaceholderText("Confirm password"),
"password123#"
);
userEvent.type(
screen.getByRole("textbox", { name: "Full name" }),
"Gnar Dog"
@ -94,8 +97,8 @@ describe("AdminDetails - form", () => {
expect(onSubmitSpy).toHaveBeenCalledWith({
email: "hi@gnar.dog",
name: "Gnar Dog",
password: "p@ssw0rd",
password_confirmation: "p@ssw0rd",
password: "password123#",
password_confirmation: "password123#",
});
});
});

View file

@ -148,7 +148,7 @@ class RegistrationForm extends Component {
/>
</div>
<div className={confirmationContainerClass}>
<h2>Success</h2>
<h2>Confirm configuration</h2>
<ConfirmationPage
formData={formData}
handleSubmit={onSubmitConfirmation}

View file

@ -38,6 +38,6 @@ describe("RegistrationForm - component", () => {
container.querySelectorAll(".user-registration__container--confirmation")
.length
).toEqual(1);
expect(screen.getByText("Success")).toBeInTheDocument();
expect(screen.getByText("Confirm configuration")).toBeInTheDocument();
});
});

View file

@ -31,7 +31,7 @@ class ResetPasswordForm extends Component {
className={`${baseClass}__input`}
type="password"
hint={[
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
/>
<InputFieldWithIcon

View file

@ -5,7 +5,7 @@ import userEvent from "@testing-library/user-event";
import ResetPasswordForm from "./ResetPasswordForm";
describe("ResetPasswordForm - component", () => {
const newPassword = "p@ssw0rd";
const newPassword = "password123!";
const submitSpy = jest.fn();
it("renders correctly", () => {
render(<ResetPasswordForm handleSubmit={submitSpy} />);

View file

@ -4,7 +4,7 @@ const SYMBOL_PRESENT = /\W+/;
export default (password = "") => {
return (
password.length >= 7 &&
password.length >= 12 &&
LETTER_PRESENT.test(password) &&
NUMBER_PRESENT.test(password) &&
SYMBOL_PRESENT.test(password)

View file

@ -28,11 +28,11 @@ describe("validPassword", () => {
});
});
it("is valid if the password is at least 7 characters and includes a number and a symbol", () => {
it("is valid if the password is at least 12 characters and includes a number and a symbol", () => {
const validPasswords = [
"p@assw0rd",
"p@assw0rd123",
"This should be v4lid!",
"admin123.",
"admin123.pass",
"pRZ'bW,6'6o}HnpL62",
];

View file

@ -45,6 +45,8 @@ const ConfirmInvitePage = ({
const { create } = usersAPI;
const { LOGIN } = paths;
setUserErrors({});
try {
await create(formData);

View file

@ -53,7 +53,7 @@ const LoginPreviewPage = ({ router }: ILoginPreviewPageProps): JSX.Element => {
if (isPreviewMode) {
onSubmit({
email: "admin@example.com",
password: "admin123#",
password: "preview1337#",
});
}
}, []);

View file

@ -50,8 +50,10 @@ const RegistrationPage = ({ router }: IRegistrationPageProps) => {
setCurrentUser(user);
setAvailableTeams(available_teams);
return router.push(MANAGE_HOSTS);
} catch (response) {
console.error(response);
} catch (error) {
// TODO: Alert user to server errors
console.log(error);
setPage(1);
return false;
}
};

View file

@ -73,7 +73,7 @@ const ResetPasswordPage = ({ location, router }: IResetPasswordPageProps) => {
return (
<AuthenticationFormWrapper>
<StackedWhiteBoxes leadText="Create a new password. Your new password must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)">
<StackedWhiteBoxes leadText="Create a new password. Your new password must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)">
<ResetPasswordForm
handleSubmit={onSubmit}
onChangeFunc={onResetErrors}

View file

@ -243,6 +243,12 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
setCreateUserErrors({
email: "A user with this email address already exists",
});
} else if (
userErrors.data.errors[0].reason.includes("required criteria")
) {
setCreateUserErrors({
password: "Password must meet the criteria below",
});
} else {
renderFlash("error", "Could not create user. Please try again.");
}
@ -269,6 +275,12 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
setCreateUserErrors({
email: "A user with this email address already exists",
});
} else if (
userErrors.data.errors[0].reason.includes("required criteria")
) {
setCreateUserErrors({
password: "Password must meet the criteria below",
});
} else {
renderFlash("error", "Could not create user. Please try again.");
}
@ -298,6 +310,12 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
setEditUserErrors({
email: "A user with this email address already exists",
});
} else if (
userErrors.data.errors[0].reason.includes("required criteria")
) {
setEditUserErrors({
password: "Password must meet the criteria below",
});
} else {
renderFlash(
"error",
@ -326,6 +344,12 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
setEditUserErrors({
email: "A user with this email address already exists",
});
} else if (
userErrors.data.errors[0].reason.includes("required criteria")
) {
setEditUserErrors({
password: "Password must meet the criteria below",
});
}
renderFlash(
"error",
@ -355,6 +379,12 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
setEditUserErrors({
email: "A user with this email address already exists",
});
} else if (
userErrors.data.errors[0].reason.includes("required criteria")
) {
setEditUserErrors({
password: "Password must meet the criteria below",
});
} else {
renderFlash(
"error",

View file

@ -424,7 +424,7 @@ const UserForm = ({
value={formData.password || ""}
type="password"
hint={[
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
blockAutoComplete
/>
@ -507,7 +507,7 @@ const UserForm = ({
value={formData.password || ""}
type="password"
hint={[
"Must include 7 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
"Must include 12 characters, at least 1 number (e.g. 0 - 9), and at least 1 symbol (e.g. &*#)",
]}
blockAutoComplete
tooltip={`\

View file

@ -258,8 +258,19 @@ func (p UserPayload) User(keySize, cost int) (*User, error) {
Email: *p.Email,
Teams: []UserTeam{},
}
if err := user.SetPassword(*p.Password, keySize, cost); err != nil {
return nil, err
if (p.SSOInvite != nil && *p.SSOInvite) || (p.SSOEnabled != nil && *p.SSOEnabled) {
user.SSOEnabled = true
// SSO user requires a stand-in password to satisfy `NOT NULL` constraint
err := user.SetFakePassword(keySize, cost)
if err != nil {
return nil, err
}
} else {
err := user.SetPassword(*p.Password, keySize, cost)
if err != nil {
return nil, err
}
}
// add optional fields
@ -297,24 +308,22 @@ func (u *User) ValidatePassword(password string) error {
}
func (u *User) SetPassword(plaintext string, keySize, cost int) error {
salt, err := server.GenerateRandomText(keySize)
if err != nil {
if err := ValidatePasswordRequirements(plaintext); err != nil {
return err
}
withSalt := []byte(fmt.Sprintf("%s%s", plaintext, salt))
hashed, err := bcrypt.GenerateFromPassword(withSalt, cost)
hashed, salt, err := saltAndHashPassword(keySize, plaintext, cost)
if err != nil {
return err
}
u.Salt = salt
u.Password = hashed
u.Salt = salt
return nil
}
// Requirements for user password:
// at least 7 character length
// ValidatePasswordRequirements checks the provided password against the following requirements:
// at least 12 character length
// at least 1 symbol
// at least 1 number
func ValidatePasswordRequirements(password string) error {
@ -332,11 +341,48 @@ func ValidatePasswordRequirements(password string) error {
}
}
if len(password) >= 7 &&
if len(password) >= 12 &&
number &&
symbol {
return nil
}
return errors.New("Password does not meet validation requirements")
return errors.New("Password does not meet required criteria")
}
// SetFakePassword sets a stand-in password consisting of random text generated by filling in keySize bytes with
// random data and then base64 encoding those bytes.
//
// Usage should be limited to cases such as SSO users where a stand-in password is needed to satisfy `NOT NULL` constraints.
// There is no guarantee that the generated password will otherwise satisfy complexity, length or
// other requirements of standard password validation.
func (u *User) SetFakePassword(keySize, cost int) error {
plaintext, err := server.GenerateRandomText(14)
if err != nil {
return err
}
hashed, salt, err := saltAndHashPassword(keySize, plaintext, cost)
if err != nil {
return err
}
u.Password = hashed
u.Salt = salt
return nil
}
func saltAndHashPassword(keySize int, plaintext string, cost int) (hashed []byte, salt string, err error) {
salt, err = server.GenerateRandomText(keySize)
if err != nil {
return nil, "", err
}
withSalt := []byte(fmt.Sprintf("%s%s", plaintext, salt))
hashed, err = bcrypt.GenerateFromPassword(withSalt, cost)
if err != nil {
return nil, "", err
}
return hashed, salt, nil
}

View file

@ -5,12 +5,12 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
)
func TestValidatePassword(t *testing.T) {
var passwordTests = []struct {
passwordTests := []struct {
Password, Email string
Admin, PasswordReset bool
}{
@ -62,6 +62,10 @@ func TestUserPasswordRequirements(t *testing.T) {
},
{
password: "foobarbaz!3",
wantErr: true,
},
{
password: "foobarbaz!3!",
},
}
@ -76,3 +80,21 @@ func TestUserPasswordRequirements(t *testing.T) {
})
}
}
func TestSaltAndHashPassword(t *testing.T) {
passwordTests := []string{"foobar!!", "bazbing!!"}
keySize := 24
cost := 10
for _, pwd := range passwordTests {
hashed, salt, err := saltAndHashPassword(keySize, pwd, cost)
require.NoError(t, err)
saltAndPass := []byte(fmt.Sprintf("%s%s", pwd, salt))
err = bcrypt.CompareHashAndPassword(hashed, saltAndPass)
require.NoError(t, err)
err = bcrypt.CompareHashAndPassword(hashed, []byte(fmt.Sprint("invalidpassword", salt)))
require.Error(t, err)
}
}

View file

@ -111,7 +111,7 @@ func (s *integrationTestSuite) TestDoubleUserCreationErrors() {
params := fleet.UserPayload{
Name: ptr.String("user1"),
Email: ptr.String("email@asd.com"),
Password: ptr.String("pass"),
Password: &test.GoodPassword,
GlobalRole: ptr.String(fleet.RoleObserver),
}
@ -127,7 +127,7 @@ func (s *integrationTestSuite) TestUserWithoutRoleErrors() {
params := fleet.UserPayload{
Name: ptr.String("user1"),
Email: ptr.String("email@asd.com"),
Password: ptr.String("pass"),
Password: ptr.String(test.GoodPassword),
}
resp := s.Do("POST", "/api/latest/fleet/users/admin", &params, http.StatusUnprocessableEntity)
@ -140,7 +140,7 @@ func (s *integrationTestSuite) TestUserWithWrongRoleErrors() {
params := fleet.UserPayload{
Name: ptr.String("user1"),
Email: ptr.String("email@asd.com"),
Password: ptr.String("pass"),
Password: ptr.String(test.GoodPassword),
GlobalRole: ptr.String("wrongrole"),
}
resp := s.Do("POST", "/api/latest/fleet/users/admin", &params, http.StatusUnprocessableEntity)
@ -162,7 +162,7 @@ func (s *integrationTestSuite) TestUserCreationWrongTeamErrors() {
params := fleet.UserPayload{
Name: ptr.String("user2"),
Email: ptr.String("email2@asd.com"),
Password: ptr.String("pass"),
Password: ptr.String(test.GoodPassword),
Teams: &teams,
}
resp := s.Do("POST", "/api/latest/fleet/users/admin", &params, http.StatusUnprocessableEntity)
@ -1096,7 +1096,7 @@ func (s *integrationTestSuite) TestInvites() {
var createFromInviteResp createUserResponse
s.DoJSON("POST", "/api/latest/fleet/users", fleet.UserPayload{
Name: ptr.String("Full Name"),
Password: ptr.String("pass1word!"),
Password: ptr.String(test.GoodPassword),
Email: ptr.String("a@b.c"),
InviteToken: ptr.String(validInviteToken),
}, http.StatusOK, &createFromInviteResp)
@ -1121,7 +1121,7 @@ func (s *integrationTestSuite) TestInvites() {
// create user from never used but deleted invite
s.DoJSON("POST", "/api/latest/fleet/users", fleet.UserPayload{
Name: ptr.String("Full Name"),
Password: ptr.String("pass1word!"),
Password: ptr.String(test.GoodPassword),
Email: ptr.String("a@b.c"),
InviteToken: ptr.String(deletedInviteToken),
}, http.StatusNotFound, &createFromInviteResp)
@ -1158,7 +1158,7 @@ func (s *integrationTestSuite) TestCreateUserFromInviteErrors() {
"empty name",
fleet.UserPayload{
Name: ptr.String(""),
Password: ptr.String("pass1word!"),
Password: &test.GoodPassword,
Email: ptr.String("a@b.c"),
InviteToken: ptr.String(invite.Token),
},
@ -1168,7 +1168,7 @@ func (s *integrationTestSuite) TestCreateUserFromInviteErrors() {
"empty email",
fleet.UserPayload{
Name: ptr.String("Name"),
Password: ptr.String("pass1word!"),
Password: &test.GoodPassword,
Email: ptr.String(""),
InviteToken: ptr.String(invite.Token),
},
@ -1188,7 +1188,7 @@ func (s *integrationTestSuite) TestCreateUserFromInviteErrors() {
"empty token",
fleet.UserPayload{
Name: ptr.String("Name"),
Password: ptr.String("pass1word!"),
Password: &test.GoodPassword,
Email: ptr.String("a@b.c"),
InviteToken: ptr.String(""),
},
@ -1198,7 +1198,7 @@ func (s *integrationTestSuite) TestCreateUserFromInviteErrors() {
"invalid token",
fleet.UserPayload{
Name: ptr.String("Name"),
Password: ptr.String("pass1word!"),
Password: &test.GoodPassword,
Email: ptr.String("a@b.c"),
InviteToken: ptr.String("invalid"),
},
@ -2533,7 +2533,7 @@ func (s *integrationTestSuite) TestUsers() {
// create a new user
var createResp createUserResponse
userRawPwd := "pass"
userRawPwd := test.GoodPassword
params := fleet.UserPayload{
Name: ptr.String("extra"),
Email: ptr.String("extra@asd.com"),
@ -2606,7 +2606,7 @@ func (s *integrationTestSuite) TestUsers() {
// modify user - email change, password ok
params = fleet.UserPayload{
Email: ptr.String("extra2@asd.com"),
Password: ptr.String("pass"),
Password: ptr.String(test.GoodPassword),
}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/users/%d", u.ID), params, http.StatusOK, &modResp)
assert.Equal(t, u.ID, modResp.User.ID)
@ -2621,7 +2621,7 @@ func (s *integrationTestSuite) TestUsers() {
// perform a required password change as the user themselves
s.token = s.getTestToken(u.Email, userRawPwd)
var perfPwdResetResp performRequiredPasswordResetResponse
newRawPwd := "new_password!"
newRawPwd := test.GoodPassword2
s.DoJSON("POST", "/api/latest/fleet/perform_required_password_reset", performRequiredPasswordResetRequest{
Password: newRawPwd,
ID: u.ID,
@ -3825,7 +3825,7 @@ func (s *integrationTestSuite) TestGlobalPoliciesBrowsing() {
},
},
}
password := "p4ssw0rd."
password := test.GoodPassword
require.NoError(t, teamObserver.SetPassword(password, 10, 10))
_, err = s.ds.NewUser(context.Background(), teamObserver)
require.NoError(t, err)
@ -4446,7 +4446,7 @@ func (s *integrationTestSuite) TestPasswordReset() {
// create a new user
var createResp createUserResponse
userRawPwd := "passw0rd!"
userRawPwd := test.GoodPassword
params := fleet.UserPayload{
Name: ptr.String("forgotpwd"),
Email: ptr.String("forgotpwd@example.com"),
@ -4478,7 +4478,7 @@ func (s *integrationTestSuite) TestPasswordReset() {
})
// proceed with reset password
userNewPwd := "newpassw0rd!"
userNewPwd := test.GoodPassword2
res = s.DoRawNoAuth("POST", "/api/latest/fleet/reset_password", jsonMustMarshal(t, resetPasswordRequest{PasswordResetToken: token, NewPassword: userNewPwd}), http.StatusOK)
res.Body.Close()
@ -4601,7 +4601,7 @@ func (s *integrationTestSuite) TestModifyUser() {
// create a new user
var createResp createUserResponse
userRawPwd := "passw0rd!"
userRawPwd := test.GoodPassword
params := fleet.UserPayload{
Name: ptr.String("moduser"),
Email: ptr.String("moduser@example.com"),
@ -4639,7 +4639,7 @@ func (s *integrationTestSuite) TestModifyUser() {
require.Equal(t, u.Email, modResp.User.Email) // new email is pending confirmation, not changed immediately
// as the user: set new password without providing current one
newRawPwd := userRawPwd + "2"
newRawPwd := test.GoodPassword2
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/users/%d", u.ID), fleet.UserPayload{
NewPassword: ptr.String(newRawPwd),
}, http.StatusUnprocessableEntity, &modResp)

View file

@ -11,6 +11,7 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -161,7 +162,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamPolicies() {
s.token = oldToken
})
password := "garbage"
password := test.GoodPassword
email := "testteam@user.com"
u := &fleet.User{
@ -271,7 +272,7 @@ func (s *integrationEnterpriseTestSuite) TestAvailableTeams() {
Email: "available@example.com",
GlobalRole: ptr.String("observer"),
}
err = user.SetPassword("foobar123#", 10, 10)
err = user.SetPassword(test.GoodPassword, 10, 10)
require.Nil(t, err)
user, err = s.ds.NewUser(context.Background(), user)
require.Nil(t, err)
@ -381,7 +382,7 @@ func (s *integrationEnterpriseTestSuite) TestTeamEndpoints() {
Email: "user@example.com",
GlobalRole: ptr.String("observer"),
}
require.NoError(t, user.SetPassword("foobar123#", 10, 10))
require.NoError(t, user.SetPassword(test.GoodPassword, 10, 10))
user, err := s.ds.NewUser(context.Background(), user)
require.NoError(t, err)

View file

@ -3,8 +3,6 @@ package service
import (
"context"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
@ -31,21 +29,11 @@ func (svc *Service) CreateInitialUser(ctx context.Context, p fleet.UserPayload)
}
func (svc *Service) newUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, error) {
var ssoEnabled bool
// if user is SSO generate a fake password
if (p.SSOInvite != nil && *p.SSOInvite) || (p.SSOEnabled != nil && *p.SSOEnabled) {
fakePassword, err := server.GenerateRandomText(14)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "generate stand-in password")
}
p.Password = &fakePassword
ssoEnabled = true
}
user, err := p.User(svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
if err != nil {
return nil, err
}
user.SSOEnabled = ssoEnabled
user, err = svc.ds.NewUser(ctx, user)
if err != nil {
return nil, err

View file

@ -19,6 +19,7 @@ import (
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/async"
"github.com/fleetdm/fleet/v4/server/sso"
"github.com/fleetdm/fleet/v4/server/test"
kitlog "github.com/go-kit/kit/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -130,17 +131,17 @@ var testUsers = map[string]struct {
GlobalRole *string
}{
"admin1": {
PlaintextPassword: "foobarbaz1234!",
PlaintextPassword: test.GoodPassword,
Email: "admin1@example.com",
GlobalRole: ptr.String(fleet.RoleAdmin),
},
"user1": {
PlaintextPassword: "foobarbaz1234!",
PlaintextPassword: test.GoodPassword,
Email: "user1@example.com",
GlobalRole: ptr.String(fleet.RoleMaintainer),
},
"user2": {
PlaintextPassword: "bazfoo1234!",
PlaintextPassword: test.GoodPassword,
Email: "user2@example.com",
GlobalRole: ptr.String(fleet.RoleObserver),
},

View file

@ -809,6 +809,10 @@ func (svc *Service) PerformRequiredPasswordReset(ctx context.Context, password s
return nil, fleet.NewInvalidArgumentError("new_password", "cannot reuse old password")
}
if err := fleet.ValidatePasswordRequirements(password); err != nil {
return nil, fleet.NewInvalidArgumentError("new_password", "Password does not meet required criteria")
}
user.AdminForcedPasswordReset = false
err := svc.setNewPassword(ctx, user, password)
if err != nil {
@ -895,9 +899,10 @@ func (svc *Service) ResetPassword(ctx context.Context, token, password string) e
return fleet.NewInvalidArgumentError("new_password", "cannot reuse old password")
}
// password requirements are validated as part of `setNewPassword``
err = svc.setNewPassword(ctx, user, password)
if err != nil {
return ctxerr.Wrap(ctx, err, "setting new password")
return fleet.NewInvalidArgumentError("new_password", err.Error())
}
// delete password reset tokens for user

View file

@ -302,14 +302,15 @@ func TestUserAuth(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
tt.user.SetPassword("p4ssw0rd.", 10, 10)
err := tt.user.SetPassword(test.GoodPassword, 10, 10)
require.NoError(t, err)
// To test a user reading/modifying itself.
u := *tt.user
self = &u
// A user can always read itself (read rego action).
_, err := svc.User(ctx, tt.user.ID)
_, err = svc.User(ctx, tt.user.ID)
require.NoError(t, err)
// A user can always write itself (write rego action).
@ -317,7 +318,7 @@ func TestUserAuth(t *testing.T) {
require.NoError(t, err)
// A user can always change its own password (change_password rego action).
_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{Password: ptr.String("p4ssw0rd."), NewPassword: ptr.String("p4ssw0rd.3")})
_, err = svc.ModifyUser(ctx, tt.user.ID, fleet.UserPayload{Password: ptr.String(test.GoodPassword), NewPassword: ptr.String(test.GoodPassword2)})
require.NoError(t, err)
changeRole := func(role string) string {
@ -353,7 +354,7 @@ func TestUserAuth(t *testing.T) {
_, err = svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Name"),
Email: ptr.String("some@email.com"),
Password: ptr.String("passw0rd."),
Password: ptr.String(test.GoodPassword),
Teams: &teams,
})
checkAuthErr(t, tt.shouldFailTeamWrite, err)
@ -361,7 +362,7 @@ func TestUserAuth(t *testing.T) {
_, err = svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Name"),
Email: ptr.String("some@email.com"),
Password: ptr.String("passw0rd."),
Password: ptr.String(test.GoodPassword),
GlobalRole: ptr.String(fleet.RoleAdmin),
})
checkAuthErr(t, tt.shouldFailGlobalWrite, err)
@ -403,10 +404,10 @@ func TestUserAuth(t *testing.T) {
_, err = svc.RequirePasswordReset(ctx, userTeamMaintainerID, false)
checkAuthErr(t, tt.shouldFailTeamPasswordReset, err)
_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{NewPassword: ptr.String("passw0rd.2")})
_, err = svc.ModifyUser(ctx, userGlobalMaintainerID, fleet.UserPayload{NewPassword: ptr.String(test.GoodPassword2)})
checkAuthErr(t, tt.shouldFailGlobalChangePassword, err)
_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{NewPassword: ptr.String("passw0rd.2")})
_, err = svc.ModifyUser(ctx, userTeamMaintainerID, fleet.UserPayload{NewPassword: ptr.String(test.GoodPassword2)})
checkAuthErr(t, tt.shouldFailTeamChangePassword, err)
_, err = svc.ListUsers(ctx, fleet.UserListOptions{})
@ -423,7 +424,8 @@ func TestModifyUserEmail(t *testing.T) {
ID: 3,
Email: "foo@bar.com",
}
user.SetPassword("password", 10, 10)
err := user.SetPassword(test.GoodPassword, 10, 10)
require.NoError(t, err)
ms := new(mock.Store)
ms.PendingEmailChangeFunc = func(ctx context.Context, id uint, em, tk string) error {
return nil
@ -461,10 +463,10 @@ func TestModifyUserEmail(t *testing.T) {
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
payload := fleet.UserPayload{
Email: ptr.String("zip@zap.com"),
Password: ptr.String("password"),
Password: ptr.String(test.GoodPassword),
Position: ptr.String("minion"),
}
_, err := svc.ModifyUser(ctx, 3, payload)
_, err = svc.ModifyUser(ctx, 3, payload)
require.Nil(t, err)
assert.True(t, ms.PendingEmailChangeFuncInvoked)
assert.True(t, ms.SaveUserFuncInvoked)
@ -475,7 +477,8 @@ func TestModifyUserEmailNoPassword(t *testing.T) {
ID: 3,
Email: "foo@bar.com",
}
user.SetPassword("password", 10, 10)
err := user.SetPassword(test.GoodPassword, 10, 10)
require.NoError(t, err)
ms := new(mock.Store)
ms.PendingEmailChangeFunc = func(ctx context.Context, id uint, em, tk string) error {
return nil
@ -483,6 +486,9 @@ func TestModifyUserEmailNoPassword(t *testing.T) {
ms.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
return user, nil
}
ms.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
return user, nil
}
ms.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
config := &fleet.AppConfig{
SMTPSettings: fleet.SMTPSettings{
@ -504,9 +510,8 @@ func TestModifyUserEmailNoPassword(t *testing.T) {
payload := fleet.UserPayload{
Email: ptr.String("zip@zap.com"),
// NO PASSWORD
// Password: ptr.String("password"),
}
_, err := svc.ModifyUser(ctx, 3, payload)
_, err = svc.ModifyUser(ctx, 3, payload)
require.NotNil(t, err)
var iae *fleet.InvalidArgumentError
ok := errors.As(err, &iae)
@ -521,7 +526,8 @@ func TestModifyAdminUserEmailNoPassword(t *testing.T) {
ID: 3,
Email: "foo@bar.com",
}
user.SetPassword("password", 10, 10)
err := user.SetPassword(test.GoodPassword, 10, 10)
require.NoError(t, err)
ms := new(mock.Store)
ms.PendingEmailChangeFunc = func(ctx context.Context, id uint, em, tk string) error {
return nil
@ -529,6 +535,9 @@ func TestModifyAdminUserEmailNoPassword(t *testing.T) {
ms.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
return user, nil
}
ms.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) {
return user, nil
}
ms.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
config := &fleet.AppConfig{
SMTPSettings: fleet.SMTPSettings{
@ -550,9 +559,9 @@ func TestModifyAdminUserEmailNoPassword(t *testing.T) {
payload := fleet.UserPayload{
Email: ptr.String("zip@zap.com"),
// NO PASSWORD
// Password: ptr.String("password"),
// Password: &test.TestGoodPassword,
}
_, err := svc.ModifyUser(ctx, 3, payload)
_, err = svc.ModifyUser(ctx, 3, payload)
require.NotNil(t, err)
var iae *fleet.InvalidArgumentError
ok := errors.As(err, &iae)
@ -567,7 +576,8 @@ func TestModifyAdminUserEmailPassword(t *testing.T) {
ID: 3,
Email: "foo@bar.com",
}
user.SetPassword("password", 10, 10)
err := user.SetPassword(test.GoodPassword, 10, 10)
require.NoError(t, err)
ms := new(mock.Store)
ms.PendingEmailChangeFunc = func(ctx context.Context, id uint, em, tk string) error {
return nil
@ -601,9 +611,9 @@ func TestModifyAdminUserEmailPassword(t *testing.T) {
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
payload := fleet.UserPayload{
Email: ptr.String("zip@zap.com"),
Password: ptr.String("password"),
Password: ptr.String(test.GoodPassword),
}
_, err := svc.ModifyUser(ctx, 3, payload)
_, err = svc.ModifyUser(ctx, 3, payload)
require.Nil(t, err)
assert.True(t, ms.PendingEmailChangeFuncInvoked)
assert.True(t, ms.SaveUserFuncInvoked)
@ -639,7 +649,7 @@ func testUsersCreateUserForcePasswdReset(t *testing.T, ds *mysql.Datastore) {
Email: "admin@foo.com",
GlobalRole: ptr.String(fleet.RoleAdmin),
}
err := admin.SetPassword("p4ssw0rd.", 10, 10)
err := admin.SetPassword(test.GoodPassword, 10, 10)
require.NoError(t, err)
admin, err = ds.NewUser(context.Background(), admin)
require.NoError(t, err)
@ -649,7 +659,7 @@ func testUsersCreateUserForcePasswdReset(t *testing.T, ds *mysql.Datastore) {
user, err := svc.CreateUser(ctx, fleet.UserPayload{
Name: ptr.String("Some Observer"),
Email: ptr.String("some-observer@email.com"),
Password: ptr.String("passw0rd."),
Password: ptr.String(test.GoodPassword),
GlobalRole: ptr.String(fleet.RoleObserver),
})
require.NoError(t, err)
@ -671,29 +681,29 @@ func testUsersChangePassword(t *testing.T, ds *mysql.Datastore) {
}{
{ // all good
user: users["admin1@example.com"],
oldPassword: "foobarbaz1234!",
newPassword: "12345cat!",
oldPassword: test.GoodPassword,
newPassword: test.GoodPassword2,
},
{ // prevent password reuse
user: users["admin1@example.com"],
oldPassword: "12345cat!",
newPassword: "foobarbaz1234!",
oldPassword: test.GoodPassword2,
newPassword: test.GoodPassword,
wantErr: fleet.NewInvalidArgumentError("new_password", "cannot reuse old password"),
},
{ // all good
user: users["user1@example.com"],
oldPassword: "foobarbaz1234!",
newPassword: "newpassa1234!",
oldPassword: test.GoodPassword,
newPassword: test.GoodPassword2,
},
{ // bad old password
user: users["user1@example.com"],
oldPassword: "wrong_password",
newPassword: "12345cat!",
newPassword: test.GoodPassword2,
anyErr: true,
},
{ // missing old password
user: users["user1@example.com"],
newPassword: "123cataaa!",
newPassword: test.GoodPassword2,
wantErr: fleet.NewInvalidArgumentError("old_password", "Old password cannot be empty"),
},
}
@ -790,7 +800,7 @@ func TestPerformRequiredPasswordReset(t *testing.T) {
_, err = svc.RequirePasswordReset(ctx, user.ID, false)
require.Nil(t, err)
ctx = refreshCtx(t, ctx, user, ds, session)
_, err = svc.PerformRequiredPasswordReset(ctx, "new_pass")
_, err = svc.PerformRequiredPasswordReset(ctx, test.GoodPassword2)
require.NotNil(t, err)
_, err = svc.RequirePasswordReset(ctx, user.ID, true)
@ -802,14 +812,14 @@ func TestPerformRequiredPasswordReset(t *testing.T) {
require.Equal(t, "validation failed: new_password cannot reuse old password", err.Error())
// should succeed with good new password
u, err := svc.PerformRequiredPasswordReset(ctx, "new_pass")
u, err := svc.PerformRequiredPasswordReset(ctx, test.GoodPassword2)
require.Nil(t, err)
assert.False(t, u.AdminForcedPasswordReset)
ctx = context.Background()
// Now user should be able to login with new password
u, _, err = svc.Login(ctx, tt.Email, "new_pass")
u, _, err = svc.Login(ctx, tt.Email, test.GoodPassword2)
require.Nil(t, err)
assert.False(t, u.AdminForcedPasswordReset)
})
@ -828,20 +838,20 @@ func TestResetPassword(t *testing.T) {
}{
{ // all good
token: "abcd",
newPassword: "123cat!",
newPassword: test.GoodPassword2,
},
{ // prevent reuse
token: "abcd",
newPassword: "123cat!",
newPassword: test.GoodPassword2,
wantErr: fleet.NewInvalidArgumentError("new_password", "cannot reuse old password"),
},
{ // bad token
token: "dcbaz",
newPassword: "123cat!",
newPassword: test.GoodPassword,
wantErr: sql.ErrNoRows,
},
{ // missing token
newPassword: "123cat!",
newPassword: test.GoodPassword,
wantErr: fleet.NewInvalidArgumentError("token", "Token cannot be empty field"),
},
}

View file

@ -6,7 +6,9 @@ import (
)
var (
UserNoRoles = &fleet.User{
GoodPassword = "password123#"
GoodPassword2 = "password123!"
UserNoRoles = &fleet.User{
ID: 1,
}
UserAdmin = &fleet.User{

View file

@ -32,7 +32,7 @@ create_user_endpoint="api/latest/fleet/users/admin"
data='{
"name": "Andre Verot",
"email": "andre@thecompany.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"teams": [
@ -48,7 +48,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Joanne Jackson",
"email": "jo@thecompany.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"teams": [
@ -64,7 +64,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Cheryl Gardner",
"email": "cheryl87@domain.tld",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"teams": [
@ -80,7 +80,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Lisa Walsh",
"email": "lisa_walsh@domain.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"teams": [
@ -104,7 +104,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Christopher Mitchell",
"email": "christopher98@thecompany.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": "admin"
}'
@ -114,7 +114,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Kai Boucher",
"email": "boucher_@thecompany.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null
}'
@ -124,7 +124,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Henry Lewis",
"email": "henry.lewis@thecompany.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"teams": [
@ -140,7 +140,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Shintaro Sato",
"email": "shin-sato@thecompany.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"teams": [
@ -160,7 +160,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Rosie Thomas",
"email": "rosie@thecompany.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": "maintainer"
}'
@ -170,7 +170,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Pat Moreno",
"email": "pat-moreno@thecompany.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"teams": [
@ -186,7 +186,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Mohammad Patel",
"email": "mo-patel@thecompany.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": "observer"
}'

View file

@ -11,7 +11,7 @@ create_user_endpoint="api/latest/fleet/users/admin"
data='{
"name": "Anna",
"email": "anna@organization.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": "admin",
"admin_forced_password_reset": false
@ -22,7 +22,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Mary",
"email": "mary@organization.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": "maintainer",
"admin_forced_password_reset": false
@ -33,7 +33,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Oliver",
"email": "oliver@organization.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": "observer",
"admin_forced_password_reset": false

View file

@ -26,7 +26,7 @@ create_user_endpoint="api/latest/fleet/users/admin"
data='{
"name": "Anna",
"email": "anna@organization.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": "admin",
"admin_forced_password_reset": false
@ -37,7 +37,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Mary",
"email": "mary@organization.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": "maintainer",
"admin_forced_password_reset": false
@ -48,7 +48,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Oliver",
"email": "oliver@organization.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": "observer",
"admin_forced_password_reset": false
@ -59,7 +59,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Marco",
"email": "marco@organization.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"admin_forced_password_reset": false,
@ -80,7 +80,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Anita T. Admin",
"email": "anita@organization.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"admin_forced_password_reset": false,
@ -97,7 +97,7 @@ curl -X POST $CURL_FLAGS -H "Authorization: Bearer $TOKEN" "$SERVER_URL/$create_
data='{
"name": "Toni",
"email": "toni@organization.com",
"password": "user123#",
"password": "password123#",
"invited_by": 1,
"global_role": null,
"admin_forced_password_reset": false,

View file

@ -50,7 +50,7 @@
<p>The Fleet UI is now available at <a href="http://localhost:1337"
target="_blank">http://localhost:1337</a>. Use the credentials below to login.</p>
<p class="mb-2"><strong>Email:</strong> admin@example.com</p>
<p><strong>Password:</strong> admin123#</p>
<p><strong>Password:</strong> preview1337#</p>
</div>
<div style="padding-top: 60px;">
<h2 class="mb-4">Next steps</h2>