Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Added support for password expiration feature #713 #724

Merged
merged 8 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:

- name: Get openwisp-radius
run: |
curl -L https://github.com/openwisp/openwisp-radius/tarball/master -o openwisp-radius.tar.gz
curl -L https://github.com/openwisp/openwisp-radius/tarball/issues/491-password-expiration -o openwisp-radius.tar.gz
tar -xvzf openwisp-radius.tar.gz && mkdir openwisp-radius
mv openwisp-*/* openwisp-radius

Expand Down Expand Up @@ -108,7 +108,9 @@ jobs:
yarn start &

- name: Running OpenWISP Radius
run: cd openwisp-radius && ./tests/manage.py runserver &
run: |
cp browser-test/local_settings.py openwisp-radius/tests/openwisp2/local_settings.py \
&& cd openwisp-radius && ./tests/manage.py runserver &

- name: geckodriver/firefox
run: |
Expand Down
20 changes: 19 additions & 1 deletion browser-test/initialize_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def load_test_data():
# do not initialize data for registration tests
registration_tests = 'register' in sys.argv
create_mobile_verification_org = 'mobileVerification' in sys.argv
expired_password_tests = 'expiredPassword' in sys.argv

sys.path.insert(0, os.path.join(OPENWISP_RADIUS_PATH, 'tests'))
sys.argv.insert(1, 'browser-test')
Expand All @@ -37,6 +38,7 @@ def load_test_data():
sys.exit(1)

from django.contrib.auth import get_user_model
from django.utils.timezone import now, timedelta
from swapper import load_model

User = get_user_model()
Expand Down Expand Up @@ -86,6 +88,22 @@ def load_test_data():
sys.exit(2)

user = User.objects.create_user(
username=test_user_email, password=test_user_password, email=test_user_email
username=test_user_email,
password=test_user_password,
email=test_user_email
)
OrganizationUser.objects.create(organization=org, user=user)
User.objects.update(password_updated=now().date())

if expired_password_tests:
data = test_data['expiredPasswordUser']
expired_password_user = User.objects.create_user(
username=data['email'],
password=data['password'],
email=data['email'],
password_updated=now().date()-timedelta(days=180)
)
OrganizationUser.objects.create(
organization=org,
user=expired_password_user
)
2 changes: 2 additions & 0 deletions browser-test/local_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OPENWISP_USERS_USER_PASSWORD_EXPIRATION = 1
OPENWISP_USERS_STAFF_USER_PASSWORD_EXPIRATION = 1
91 changes: 91 additions & 0 deletions browser-test/password-expired.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {until} from "selenium-webdriver";
import {
getDriver,
getElementByCss,
urls,
initialData,
initializeData,
tearDown,
successToastSelector,
} from "./utils";

describe("Selenium tests for expired password flow />", () => {
let driver;

beforeAll(async () => {
await initializeData("expiredPassword");
driver = await getDriver();
}, 30000);

afterAll(async () => {
await tearDown(driver);
});

it("should force user to change password before captive portal login", async () => {
// login with original password
await driver.get(urls.login);
const data = initialData();
let username = await getElementByCss(driver, "input#username");
username.sendKeys(data.expiredPasswordUser.email);
let password = await getElementByCss(driver, "input#password");
password.sendKeys(data.expiredPasswordUser.password);
let submitBtn = await getElementByCss(driver, "input[type=submit]");
submitBtn.click();
await driver.wait(until.urlContains("change-password"), 5000);
let successToastDiv = await getElementByCss(driver, "div[role=alert]");
await driver.wait(until.elementIsVisible(successToastDiv));
expect(await successToastDiv.getText()).toEqual("Login successful");
const warningToastMessage = await getElementByCss(
driver,
".Toastify__toast--warning",
);
await driver.wait(until.elementIsVisible(warningToastMessage));
expect(await warningToastMessage.getText()).toEqual(
"Your password has expired, please update it.",
);

// Try visiting the status page, but the user should redirected
// back to change password page
await driver.get(urls.status);
await driver.wait(until.urlContains("change-password"), 5000);

// changing password
await getElementByCss(driver, "div#password-change");
const currPassword = await getElementByCss(
driver,
"input#current-password",
);
currPassword.sendKeys(data.expiredPasswordUser.password);
const newPassword = "newPassword@";
const changePassword = await getElementByCss(driver, "input#new-password");
changePassword.sendKeys(newPassword);
const changePasswordConfirm = await getElementByCss(
driver,
"input#password-confirm",
);
changePasswordConfirm.sendKeys(newPassword);
submitBtn = await getElementByCss(driver, "input[type=submit]");
submitBtn.click();
await getElementByCss(driver, "div#status");
successToastDiv = await getElementByCss(driver, successToastSelector);
await driver.wait(until.elementIsVisible(successToastDiv));
expect(await successToastDiv.getText()).toEqual(
"Password updated successfully",
);

// login with new password
await driver.manage().deleteAllCookies();
await driver.get(urls.login);
await driver.wait(until.urlContains("login"), 5000);
username = await getElementByCss(driver, "input#username");
username.sendKeys(data.expiredPasswordUser.email);
password = await getElementByCss(driver, "input#password");
password.sendKeys(newPassword);
submitBtn = await getElementByCss(driver, "input[type=submit]");
submitBtn.click();
await getElementByCss(driver, "div#status");
successToastDiv = await getElementByCss(driver, "div[role=alert]");
await driver.wait(until.elementIsVisible(successToastDiv));
expect(await successToastDiv.getText()).toEqual("Login successful");
});
});
5 changes: 5 additions & 0 deletions browser-test/testData.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
"password": "testuser",
"organization": "default"
},
"expiredPasswordUser": {
"email": "[email protected]",
"password": "testuser",
"organization": "default"
},
"mobileVerificationTestUser": {
"phoneNumber": "+911234567890",
"password": "testuser",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<PasswordChange /> rendering should not show 'cancel' button if password is expired 1`] = `
<div
className="container content"
id="password-change"
>
<div
className="inner"
>
<form
className="main-column"
onSubmit={[Function]}
>
<div
className="inner"
>
<h1>
Change your password
</h1>
<div
className="row current-password"
>
<label
htmlFor="current-password"
>
Current password
</label>
<input
autoComplete="password"
className="input"
id="current-password"
name="currentPassword"
onChange={[Function]}
pattern=".{6,}"
placeholder="Your current password"
required={true}
title="password must be a minimum of 6 characters"
type="password"
value=""
/>
<PasswordToggleIcon
hidePassword={true}
inputRef={
Object {
"current": null,
}
}
isVisible={false}
parentClassName=""
secondInputRef={Object {}}
toggler={[Function]}
/>
</div>
<div
className="row new-password"
>
<label
htmlFor="new-password"
>
New Password
</label>
<input
autoComplete="password"
className="input"
id="new-password"
name="newPassword1"
onChange={[Function]}
pattern=".{6,}"
placeholder="Your new password"
required={true}
title="password must be a minimum of 6 characters"
type="password"
value=""
/>
<PasswordToggleIcon
hidePassword={true}
inputRef={
Object {
"current": null,
}
}
isVisible={false}
parentClassName=""
secondInputRef={
Object {
"current": null,
}
}
toggler={[Function]}
/>
</div>
<div
className="row password-confirm"
>
<label
htmlFor="password-confirm"
>
Confirm password
</label>
<input
autoComplete="password"
className="input"
id="password-confirm"
name="newPassword2"
onChange={[Function]}
pattern=".{6,}"
placeholder="confirm password"
required={true}
title="password must be a minimum of 6 characters"
type="password"
value=""
/>
<PasswordToggleIcon
hidePassword={true}
inputRef={
Object {
"current": null,
}
}
isVisible={false}
parentClassName=""
secondInputRef={
Object {
"current": null,
}
}
toggler={[Function]}
/>
</div>
<div
className="row submit"
>
<input
className="button full"
type="submit"
value="Change Password"
/>
</div>
</div>
</form>
<Connect(Contact) />
</div>
</div>
`;

exports[`<PasswordChange /> rendering should render correctly 1`] = `
<div
className="container content"
Expand Down
28 changes: 13 additions & 15 deletions client/components/password-change/password-change.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,12 @@ export default class PasswordChange extends React.Component {

async componentDidMount() {
const {setLoading} = this.context;
const {
setTitle,
orgName,
cookies,
userData,
setUserData,
logout,
orgSlug,
language,
} = this.props;
const {setTitle, orgName, cookies, setUserData, logout, orgSlug, language} =
this.props;
let {userData} = this.props;
setLoading(true);
setTitle(t`PWD_CHANGE_TITL`, orgName);
const {mustLogin, mustLogout, repeatLogin} = userData;
await validateToken(
cookies,
orgSlug,
Expand All @@ -59,6 +53,8 @@ export default class PasswordChange extends React.Component {
logout,
language,
);
({userData} = this.props);
setUserData({...userData, mustLogin, mustLogout, repeatLogin});
setLoading(false);
}

Expand Down Expand Up @@ -224,11 +220,13 @@ export default class PasswordChange extends React.Component {
/>
</div>

<div className="row cancel">
<Link className="button full" to={`/${orgSlug}/status`}>
{t`CANCEL`}
</Link>
</div>
{userData.password_expired !== true && (
<div className="row cancel">
<Link className="button full" to={`/${orgSlug}/status`}>
{t`CANCEL`}
</Link>
</div>
)}
</div>
</form>

Expand Down
8 changes: 8 additions & 0 deletions client/components/password-change/password-change.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ describe("<PasswordChange /> rendering", () => {
const wrapper = createShallow(props);
expect(wrapper).toMatchSnapshot();
});

it("should not show 'cancel' button if password is expired", async () => {
props = createTestProps();
props.userData.password_expired = true;
loadTranslation("en", "default");
const wrapper = createShallow(props);
expect(wrapper).toMatchSnapshot();
});
});

describe("<PasswordChange /> interactions", () => {
Expand Down
Loading
Loading