mirror of
https://github.com/bringout/oca-server-auth.git
synced 2026-04-18 14:32:03 +02:00
Initial commit: OCA Server Auth packages (29 packages)
This commit is contained in:
commit
3ed80311c4
1325 changed files with 127292 additions and 0 deletions
|
|
@ -0,0 +1,7 @@
|
|||
from . import test_change_password
|
||||
from . import test_res_users
|
||||
from . import test_login
|
||||
from . import test_password_history
|
||||
from . import test_reset_password
|
||||
from . import test_signup
|
||||
from . import test_migration
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
# Copyright 2023 Onestein (<https://www.onestein.eu>)
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
import re
|
||||
from unittest import mock
|
||||
|
||||
from odoo import http
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tests.common import HOST, HttpCase, Opener, get_db_name, tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestPasswordSecurityChange(HttpCase):
|
||||
def login(self, username, password):
|
||||
"""Log in with provided credentials."""
|
||||
self.session = http.root.session_store.new()
|
||||
self.opener = Opener(self.env.cr)
|
||||
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
|
||||
|
||||
with mock.patch("odoo.http.db_filter") as db_filter:
|
||||
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
|
||||
res_post = self.url_open(
|
||||
"/web/login",
|
||||
data={
|
||||
"login": username,
|
||||
"password": password,
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
},
|
||||
)
|
||||
res_post.raise_for_status()
|
||||
|
||||
return res_post
|
||||
|
||||
def test_01_empty_password_fail(self):
|
||||
"""It should fail when changing password to empty"""
|
||||
|
||||
# Log in: ensure we end up on the right page
|
||||
res_login = self.login("admin", "admin")
|
||||
self.assertEqual(res_login.request.path_url, "/web")
|
||||
self.assertEqual(res_login.status_code, 200)
|
||||
|
||||
# Change password
|
||||
user = self.env["res.users"].search([("login", "=", "admin")], limit=1)
|
||||
self.assertTrue(user)
|
||||
with self.assertRaises(UserError):
|
||||
# UserError: Setting empty passwords is not allowed for security reasons
|
||||
user._change_password("")
|
||||
|
||||
def test_02_change_password_fail(self):
|
||||
"""It should fail when changing password to weak"""
|
||||
|
||||
# Log in: ensure we end up on the right page
|
||||
res_login = self.login("admin", "admin")
|
||||
self.assertEqual(res_login.request.path_url, "/web")
|
||||
self.assertEqual(res_login.status_code, 200)
|
||||
|
||||
# Change password: error is raised because it's too short
|
||||
user = self.env["res.users"].search([("login", "=", "admin")], limit=1)
|
||||
self.assertTrue(user)
|
||||
with self.assertRaises(UserError):
|
||||
user._change_password("admin")
|
||||
|
||||
# Change password: error is raised by check password rules
|
||||
with self.assertRaises(ValidationError):
|
||||
user._change_password("adminadmin")
|
||||
|
||||
def test_03_change_password_session_expired(self):
|
||||
"""Session expires when password is changed"""
|
||||
|
||||
# Log in: ensure we end up on the right page
|
||||
res_login = self.login("admin", "admin")
|
||||
self.assertEqual(res_login.request.path_url, "/web")
|
||||
self.assertEqual(res_login.status_code, 200)
|
||||
|
||||
# Reload page: ensure we stay on the same page
|
||||
res_page1 = self.url_open("/web")
|
||||
res_page1.raise_for_status()
|
||||
self.assertEqual(res_page1.request.path_url, "/web")
|
||||
self.assertEqual(res_page1.status_code, 200)
|
||||
|
||||
# Change password: no error raised
|
||||
user = self.env["res.users"].search([("login", "=", "admin")], limit=1)
|
||||
self.assertTrue(user)
|
||||
user._change_password("!asdQWE12345_3")
|
||||
|
||||
# Try to reload page: user kicked out
|
||||
res_page2 = self.url_open("/web")
|
||||
res_page2.raise_for_status()
|
||||
self.assertEqual(res_page2.request.path_url, "/web/login")
|
||||
self.assertEqual(res_page2.status_code, 200)
|
||||
|
||||
def test_04_change_password_check_password_history(self):
|
||||
"""It should fail when chosen password was previously used"""
|
||||
|
||||
# Set password history limit
|
||||
user = self.env["res.users"].search([("login", "=", "admin")], limit=1)
|
||||
user.company_id.password_history = 3
|
||||
self.assertEqual(len(user.password_history_ids), 0)
|
||||
|
||||
# Change password: password history records created
|
||||
user._change_password("!asdQWE12345_4")
|
||||
self.assertEqual(len(user.password_history_ids), 1)
|
||||
user._change_password("!asdQWE12345_5")
|
||||
self.assertEqual(len(user.password_history_ids), 2)
|
||||
user._change_password("!asdQWE12345_6")
|
||||
self.assertEqual(len(user.password_history_ids), 3)
|
||||
user._change_password("!asdQWE12345_7")
|
||||
self.assertEqual(len(user.password_history_ids), 4)
|
||||
|
||||
# Log in: ensure we end up on the right page
|
||||
res_login = self.login("admin", "!asdQWE12345_7")
|
||||
self.assertEqual(res_login.request.path_url, "/web")
|
||||
|
||||
# Change password: reuse password in history
|
||||
with self.assertRaises(UserError):
|
||||
user._change_password("!asdQWE12345_7")
|
||||
self.assertEqual(len(user.password_history_ids), 4)
|
||||
# Change password: reuse password in history
|
||||
with self.assertRaises(UserError):
|
||||
user._change_password("!asdQWE12345_6")
|
||||
self.assertEqual(len(user.password_history_ids), 4)
|
||||
# Change password: reuse password in history
|
||||
with self.assertRaises(UserError):
|
||||
user._change_password("!asdQWE12345_5")
|
||||
self.assertEqual(len(user.password_history_ids), 4)
|
||||
|
||||
# Change password: reuse password in history but below limit
|
||||
user._change_password("!asdQWE12345_4")
|
||||
self.assertEqual(len(user.password_history_ids), 5)
|
||||
|
||||
# Try to log in with old password: it fails
|
||||
res_login1 = self.login("admin", "!asdQWE12345_7")
|
||||
self.assertEqual(res_login1.request.path_url, "/web/login")
|
||||
|
||||
# Log in with new password: ensure we end up on the right page
|
||||
res_login2 = self.login("admin", "!asdQWE12345_4")
|
||||
self.assertEqual(res_login2.request.path_url, "/web")
|
||||
|
||||
def test_20_write_password(self):
|
||||
"""Detects expected singleton errors writing passwords for more than one user"""
|
||||
users = self.env["res.users"].search([], limit=2)
|
||||
self.assertEqual(len(users), 2)
|
||||
res = users.write({"password": "!asdQWE12345"})
|
||||
self.assertTrue(res)
|
||||
|
||||
msg = re.escape(users[0].password_match_message())
|
||||
with self.assertRaisesRegex(ValidationError, msg):
|
||||
users.write({"password": "12345678"})
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
# Copyright 2023 Onestein (<https://www.onestein.eu>)
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import mock
|
||||
|
||||
from odoo import http, registry
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tests.common import HOST, HttpCase, Opener, get_db_name, new_test_user, tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestPasswordSecurityLogin(HttpCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.username = "jackoneill"
|
||||
self.passwd = "!asdQWE12345_3"
|
||||
|
||||
# Create user with strong password: no error raised
|
||||
new_test_user(self.env, self.username, password=self.passwd)
|
||||
|
||||
def login(self, username, password):
|
||||
"""Log in with provided credentials."""
|
||||
self.session = http.root.session_store.new()
|
||||
self.opener = Opener(self.env.cr)
|
||||
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
|
||||
|
||||
with mock.patch("odoo.http.db_filter") as db_filter:
|
||||
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
|
||||
res_post = self.url_open(
|
||||
"/web/login",
|
||||
data={
|
||||
"login": username,
|
||||
"password": password,
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
},
|
||||
)
|
||||
res_post.raise_for_status()
|
||||
|
||||
return res_post
|
||||
|
||||
def test_01_create_user_fail(self):
|
||||
"""It should fail when creating user with short password"""
|
||||
# Short password: UserError is raised
|
||||
with self.assertRaises(UserError):
|
||||
new_test_user(self.env, "new_user", password="abc")
|
||||
|
||||
def test_02_create_user_fail(self):
|
||||
"""It should fail when creating user with weak password"""
|
||||
# Weak password: ValidationError is raised
|
||||
with self.assertRaises(ValidationError):
|
||||
new_test_user(self.env, "new_user", password="abcdefgh")
|
||||
|
||||
def test_03_web_login_success(self):
|
||||
"""Allow authenticating by login"""
|
||||
|
||||
# Log in
|
||||
response = self.login(self.username, self.passwd)
|
||||
|
||||
# Ensure we end up on the right page
|
||||
self.assertEqual(response.request.path_url, "/web")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_04_web_login_fail(self):
|
||||
"""Fail authenticating with wrong password"""
|
||||
|
||||
# Try to log in
|
||||
response = self.login(self.username, "wrong")
|
||||
|
||||
# Ensure we stay on the login page
|
||||
self.assertEqual(response.request.path_url, "/web/login")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(
|
||||
"Wrong login/password",
|
||||
response.text,
|
||||
)
|
||||
|
||||
def test_05_web_login_expire_pass(self):
|
||||
"""It should expire password if necessary"""
|
||||
|
||||
# Make password expired
|
||||
three_days_ago = datetime.now() - timedelta(days=3)
|
||||
|
||||
with registry(get_db_name()).cursor() as cr:
|
||||
env = self.env(cr)
|
||||
user = env["res.users"].search([("login", "=", self.username)])
|
||||
user.password_write_date = three_days_ago
|
||||
user.company_id.password_expiration = 1
|
||||
|
||||
# Try to log in
|
||||
response = self.login(self.username, self.passwd)
|
||||
|
||||
# Ensure we end up on the password reset page
|
||||
self.assertIn("/web/reset_password", response.request.path_url)
|
||||
|
||||
def test_06_web_login_log_out_if_expired(self):
|
||||
"""It should log out user if password expired"""
|
||||
|
||||
# Log in
|
||||
response = self.login(self.username, self.passwd)
|
||||
|
||||
# Ensure we end up on the right page
|
||||
self.assertEqual(response.request.path_url, "/web")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make password expired while still logged in
|
||||
three_days_ago = datetime.now() - timedelta(days=3)
|
||||
|
||||
with registry(get_db_name()).cursor() as cr:
|
||||
env = self.env(cr)
|
||||
user = env["res.users"].search([("login", "=", self.username)])
|
||||
user.password_write_date = three_days_ago
|
||||
user.company_id.password_expiration = 1
|
||||
|
||||
# Try to access just a page
|
||||
req_page1 = self.url_open("/web")
|
||||
self.assertEqual(req_page1.request.path_url, "/web")
|
||||
self.assertEqual(req_page1.status_code, 200)
|
||||
|
||||
# Try to log in again
|
||||
response = self.login(self.username, self.passwd)
|
||||
|
||||
# Ensure we end up on the password reset page
|
||||
self.assertIn("/web/reset_password", response.request.path_url)
|
||||
|
||||
# Try to access just a page: user kicked out
|
||||
req_page2 = self.url_open("/web")
|
||||
self.assertEqual("/web/login", req_page2.request.path_url)
|
||||
self.assertEqual(req_page2.status_code, 200)
|
||||
|
||||
def test_07_web_login_redirect(self):
|
||||
"""It should redirect w/ hash to reset after expiration"""
|
||||
|
||||
# Emulate password expired
|
||||
with mock.patch(
|
||||
"odoo.addons.password_security.models.res_users.ResUsers._password_has_expired"
|
||||
) as func_password_has_expired:
|
||||
func_password_has_expired.return_value = True
|
||||
|
||||
# Try to log in
|
||||
response = self.login(self.username, self.passwd)
|
||||
|
||||
# Ensure we end up on the password reset page
|
||||
self.assertIn("/web/reset_password", response.request.path_url)
|
||||
|
||||
# Try to access just a page: user kicked out
|
||||
req_page = self.url_open("/web")
|
||||
self.assertEqual("/web/login", req_page.request.path_url)
|
||||
self.assertEqual(req_page.status_code, 200)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo.modules.migration import load_script
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class PasswordSecurityMigration(TransactionCase):
|
||||
def test_01_migration(self):
|
||||
"""Test the migration of the password_length value into minlength"""
|
||||
|
||||
# minlength has default value
|
||||
ICP = self.env["ir.config_parameter"]
|
||||
old_value = ICP.get_param("auth_password_policy.minlength")
|
||||
|
||||
if self.env["res.company"]._fields.get("password_length"):
|
||||
# set different password_length for multiple companies
|
||||
company1 = self.env["res.company"].create({"name": "company1"})
|
||||
company2 = self.env["res.company"].create({"name": "company2"})
|
||||
company3 = self.env["res.company"].create({"name": "company3"})
|
||||
company1.password_length = 8
|
||||
company2.password_length = 15
|
||||
company3.password_length = 11
|
||||
|
||||
# run migration script
|
||||
mod = load_script(
|
||||
"password_security/migrations/16.0.1.0.0/pre-migration.py",
|
||||
"pre-migration",
|
||||
)
|
||||
mod.migrate(self.env.cr, "16.0.1.0.0")
|
||||
|
||||
# minlength updated to maximum value
|
||||
new_value = ICP.get_param("auth_password_policy.minlength")
|
||||
self.assertNotEqual(int(old_value), 15)
|
||||
self.assertEqual(int(new_value), 15)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestPasswordHistory(TransactionCase):
|
||||
def test_check_password_history(self):
|
||||
# Disable all password checks except for history
|
||||
set_param = self.env["ir.config_parameter"].sudo().set_param
|
||||
set_param("auth_password_policy.minlength", 0)
|
||||
user = self.env.ref("base.user_admin")
|
||||
user.company_id.update(
|
||||
{
|
||||
"password_lower": 0,
|
||||
"password_history": 1,
|
||||
"password_numeric": 0,
|
||||
"password_special": 0,
|
||||
"password_upper": 0,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(len(user.password_history_ids), 0)
|
||||
|
||||
user.write({"password": "admin"})
|
||||
self.assertEqual(len(user.password_history_ids), 1)
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
user.write({"password": "admin"})
|
||||
user.write({"password": "admit"})
|
||||
self.assertEqual(len(user.password_history_ids), 2)
|
||||
|
||||
user.company_id.password_history = 2
|
||||
with self.assertRaises(UserError):
|
||||
user.write({"password": "admin"})
|
||||
with self.assertRaises(UserError):
|
||||
user.write({"password": "admit"})
|
||||
user.write({"password": "badminton"})
|
||||
self.assertEqual(len(user.password_history_ids), 3)
|
||||
|
||||
user.company_id.password_history = 0
|
||||
user.write({"password": "badminton"})
|
||||
self.assertEqual(len(user.password_history_ids), 4)
|
||||
|
||||
user.company_id.password_history = -1
|
||||
with self.assertRaises(UserError):
|
||||
user.write({"password": "admin"})
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
# Copyright 2015 LasLabs Inc.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
import time
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestResUsers(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestResUsers, self).setUp()
|
||||
self.login = "foslabs@example.com"
|
||||
self.partner_vals = {
|
||||
"name": "Partner",
|
||||
"is_company": False,
|
||||
"email": self.login,
|
||||
}
|
||||
self.password = "asdQWE123$%^"
|
||||
self.main_comp = self.env.ref("base.main_company")
|
||||
self.vals = {
|
||||
"name": "User",
|
||||
"login": self.login,
|
||||
"password": self.password,
|
||||
"company_id": self.main_comp.id,
|
||||
}
|
||||
self.model_obj = self.env["res.users"]
|
||||
|
||||
def _new_record(self):
|
||||
partner_id = self.env["res.partner"].create(self.partner_vals)
|
||||
self.vals["partner_id"] = partner_id.id
|
||||
return self.model_obj.create(self.vals)
|
||||
|
||||
def test_password_write_date_is_saved_on_create(self):
|
||||
rec_id = self._new_record()
|
||||
self.assertTrue(
|
||||
rec_id.password_write_date,
|
||||
"Password write date was not saved to db.",
|
||||
)
|
||||
|
||||
def test_password_write_date_is_updated_on_write(self):
|
||||
rec_id = self._new_record()
|
||||
old_write_date = rec_id.password_write_date
|
||||
time.sleep(2)
|
||||
rec_id.write({"password": "asdQWE123$%^2"})
|
||||
rec_id.invalidate_recordset()
|
||||
new_write_date = rec_id.password_write_date
|
||||
self.assertNotEqual(
|
||||
old_write_date,
|
||||
new_write_date,
|
||||
"Password write date was not updated on write.",
|
||||
)
|
||||
|
||||
def test_does_not_update_write_date_if_password_unchanged(self):
|
||||
rec_id = self._new_record()
|
||||
old_write_date = rec_id.password_write_date
|
||||
time.sleep(2)
|
||||
rec_id.write({"name": "Luser"})
|
||||
rec_id.invalidate_recordset()
|
||||
new_write_date = rec_id.password_write_date
|
||||
self.assertEqual(
|
||||
old_write_date,
|
||||
new_write_date,
|
||||
"Password not changed but write date updated anyway.",
|
||||
)
|
||||
|
||||
def test_check_password_returns_true_for_valid_password(self):
|
||||
rec_id = self._new_record()
|
||||
self.assertTrue(
|
||||
rec_id._check_password("asdQWE123$%^3"),
|
||||
"Password is valid but check failed.",
|
||||
)
|
||||
|
||||
def test_check_password_raises_error_for_invalid_password(self):
|
||||
rec_id = self._new_record()
|
||||
with self.assertRaises(UserError):
|
||||
rec_id._check_password("password")
|
||||
|
||||
def test_save_password_crypt(self):
|
||||
rec_id = self._new_record()
|
||||
self.assertEqual(
|
||||
1,
|
||||
len(rec_id.password_history_ids),
|
||||
)
|
||||
|
||||
def test_check_password_crypt(self):
|
||||
"""It should raise UserError if previously used"""
|
||||
rec_id = self._new_record()
|
||||
with self.assertRaises(UserError):
|
||||
rec_id.write({"password": self.password})
|
||||
|
||||
def test_password_is_expired_if_record_has_no_write_date(self):
|
||||
rec_id = self._new_record()
|
||||
rec_id.write({"password_write_date": None})
|
||||
rec_id.invalidate_recordset()
|
||||
self.assertTrue(
|
||||
rec_id._password_has_expired(),
|
||||
"Record has no password write date but check failed.",
|
||||
)
|
||||
|
||||
def test_an_old_password_is_expired(self):
|
||||
rec_id = self._new_record()
|
||||
old_write_date = "1970-01-01 00:00:00"
|
||||
rec_id.write({"password_write_date": old_write_date})
|
||||
rec_id.invalidate_recordset()
|
||||
self.assertTrue(
|
||||
rec_id._password_has_expired(),
|
||||
"Password is out of date but check failed.",
|
||||
)
|
||||
|
||||
def test_a_new_password_is_not_expired(self):
|
||||
rec_id = self._new_record()
|
||||
self.assertFalse(
|
||||
rec_id._password_has_expired(),
|
||||
"Password was just created but has already expired.",
|
||||
)
|
||||
|
||||
def test_expire_password_generates_token(self):
|
||||
rec_id = self._new_record()
|
||||
rec_id.sudo().action_expire_password()
|
||||
rec_id.invalidate_recordset()
|
||||
token = rec_id.partner_id.signup_token
|
||||
self.assertTrue(
|
||||
token,
|
||||
"A token was not generated.",
|
||||
)
|
||||
|
||||
def test_validate_pass_reset_error(self):
|
||||
"""It should throw UserError on reset inside min threshold"""
|
||||
rec_id = self._new_record()
|
||||
with self.assertRaises(UserError):
|
||||
rec_id._validate_pass_reset()
|
||||
|
||||
def test_validate_pass_reset_allow(self):
|
||||
"""It should allow reset pass when outside threshold"""
|
||||
rec_id = self._new_record()
|
||||
rec_id.password_write_date = "2016-01-01"
|
||||
self.assertEqual(
|
||||
True,
|
||||
rec_id._validate_pass_reset(),
|
||||
)
|
||||
|
||||
def test_validate_pass_reset_zero(self):
|
||||
"""It should allow reset pass when <= 0"""
|
||||
rec_id = self._new_record()
|
||||
rec_id.company_id.password_minimum = 0
|
||||
self.assertEqual(
|
||||
True,
|
||||
rec_id._validate_pass_reset(),
|
||||
)
|
||||
|
||||
def test_underscore_is_special_character(self):
|
||||
self.assertTrue(self.main_comp.password_special)
|
||||
rec_id = self._new_record()
|
||||
rec_id._check_password("asdQWE12345_3")
|
||||
|
||||
def test_user_with_admin_rights_can_create_users(self):
|
||||
demo = self.env.ref("base.user_demo")
|
||||
demo.groups_id |= self.env.ref("base.group_erp_manager")
|
||||
test1 = self.model_obj.with_user(demo).create(
|
||||
{
|
||||
"login": "test1",
|
||||
"name": "test1",
|
||||
}
|
||||
)
|
||||
test1.unlink()
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# Copyright 2023 Onestein (<https://www.onestein.eu>)
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from odoo import http
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import HOST, HttpCase, Opener, get_db_name, new_test_user, tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestPasswordSecurityReset(HttpCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create user with strong password: no error raised
|
||||
new_test_user(self.env, "jackoneill", password="!asdQWE12345_3")
|
||||
|
||||
def reset_password(self, username):
|
||||
"""Reset user password"""
|
||||
self.session = http.root.session_store.new()
|
||||
self.opener = Opener(self.env.cr)
|
||||
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
|
||||
|
||||
with mock.patch("odoo.http.db_filter") as db_filter:
|
||||
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
|
||||
res_post = self.url_open(
|
||||
"/web/reset_password",
|
||||
data={
|
||||
"login": username,
|
||||
"name": username,
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
},
|
||||
)
|
||||
res_post.raise_for_status()
|
||||
|
||||
return res_post
|
||||
|
||||
def test_01_reset_password_fail(self):
|
||||
"""It should fail when reset password below Minimum Hours"""
|
||||
# Enable check on Minimum Hours
|
||||
self.env.company.password_minimum = 24
|
||||
|
||||
# Reset password
|
||||
response = self.reset_password("jackoneill")
|
||||
|
||||
# Ensure we stay in the reset password page
|
||||
self.assertEqual(response.request.path_url, "/web/reset_password")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(
|
||||
"Passwords can only be reset every %s hour(s). "
|
||||
"Please contact an administrator for assistance."
|
||||
% self.env.company.password_minimum,
|
||||
response.text,
|
||||
)
|
||||
|
||||
def test_02_reset_password_success(self):
|
||||
"""It should succeed when check on Minimum Hours is disabled"""
|
||||
|
||||
# Disable check on Minimum Hours
|
||||
self.env.company.password_minimum = 0
|
||||
|
||||
# Reset password
|
||||
response = self.reset_password("jackoneill")
|
||||
|
||||
# Password reset instructions sent to user's email
|
||||
self.assertEqual(response.request.path_url, "/web/reset_password")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(
|
||||
"Password reset instructions sent to your email",
|
||||
response.text,
|
||||
)
|
||||
|
||||
def test_03_reset_password_admin(self):
|
||||
"""It should succeed when reset password is executed by Admin"""
|
||||
# Enable check on Minimum Hours
|
||||
self.env.company.password_minimum = 24
|
||||
|
||||
# Executed by Admin: no error is raised
|
||||
self.assertTrue(self.env.user._is_admin())
|
||||
self.env["res.users"].reset_password("demo")
|
||||
|
||||
# Executed by non-admin user: error is raised
|
||||
self.env = self.env(user=self.env.ref("base.user_demo"))
|
||||
self.assertFalse(self.env.user._is_admin())
|
||||
with self.assertRaises(UserError):
|
||||
self.env["res.users"].reset_password("demo")
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
# Copyright 2016 LasLabs Inc.
|
||||
# Copyright 2023 Onestein (<https://www.onestein.eu>)
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from freezegun import freeze_time
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from odoo import http
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import HOST, HttpCase, Opener, get_db_name, tagged
|
||||
|
||||
from odoo.addons.auth_signup.models.res_users import SignupError
|
||||
|
||||
|
||||
class EndTestException(Exception):
|
||||
"""It allows for isolation of resources by raise"""
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestPasswordSecuritySignup(HttpCase):
|
||||
def signup(self, username, password):
|
||||
"""Signup user"""
|
||||
self.session = http.root.session_store.new()
|
||||
self.opener = Opener(self.env.cr)
|
||||
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
|
||||
|
||||
with mock.patch("odoo.http.db_filter") as db_filter:
|
||||
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
|
||||
res_post = self.url_open(
|
||||
"/web/signup",
|
||||
data={
|
||||
"login": username,
|
||||
"name": username,
|
||||
"password": password,
|
||||
"confirm_password": password,
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
},
|
||||
)
|
||||
res_post.raise_for_status()
|
||||
|
||||
return res_post
|
||||
|
||||
def test_01_signup_user_fail(self):
|
||||
"""It should fail when signup user with weak password"""
|
||||
# Weak password: signup failed
|
||||
response = self.signup("jackoneill", "jackoneill")
|
||||
|
||||
# Ensure we stay in the signup page
|
||||
self.assertEqual(response.request.path_url, "/web/signup")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(
|
||||
"Must contain the following:",
|
||||
response.text,
|
||||
)
|
||||
|
||||
def test_02_signup_user_success(self):
|
||||
"""It should succeed when signup user with strong password"""
|
||||
# Weak password: signup failed
|
||||
response = self.signup("jackoneill", "!asdQWE12345_3")
|
||||
|
||||
# Ensure we were logged in
|
||||
self.assertEqual(
|
||||
response.request.path_url, "/web/login_successful?account_created=True"
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_03_create_user_signup(self):
|
||||
"""Password is checked when signup to create a new user"""
|
||||
partner = self.env["res.partner"].create({"name": "test partner"})
|
||||
vals = {
|
||||
"name": "Test User",
|
||||
"login": "test_user",
|
||||
"email": "test_user@odoo.com",
|
||||
"password": "test_user_password",
|
||||
"partner_id": partner.id,
|
||||
}
|
||||
|
||||
# Weak password: SignupError is raised
|
||||
with self.assertRaises(SignupError):
|
||||
self.env["res.users"].signup(vals)
|
||||
|
||||
# Stronger password: no error raised
|
||||
vals["password"] = "asdQWE12345_3"
|
||||
with freeze_time("2020-01-01"):
|
||||
login, pwd = self.env["res.users"].signup(vals)
|
||||
|
||||
# check created user
|
||||
created_user = self.env["res.users"].search([("login", "=", "test_user")])
|
||||
self.assertEqual(login, "test_user")
|
||||
password_write_date = created_user.password_write_date
|
||||
self.assertTrue(password_write_date)
|
||||
|
||||
# Weak password: ValidationError is raised
|
||||
with self.assertRaises(ValidationError):
|
||||
created_user.password = "test_user_password"
|
||||
self.assertEqual(password_write_date, created_user.password_write_date)
|
||||
|
||||
# Stronger password: no error raised
|
||||
created_user.password = "!asdQWE12345_3"
|
||||
self.assertNotEqual(password_write_date, created_user.password_write_date)
|
||||
|
||||
def test_04_web_auth_signup_invalid_qcontext(self):
|
||||
"""It should catch AttributeError"""
|
||||
|
||||
with mock.patch("odoo.http.db_filter") as db_filter:
|
||||
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
|
||||
with self.assertRaises(AttributeError):
|
||||
# 'TestPasswordSecuritySignup' object has no attribute 'session'
|
||||
self.url_open(
|
||||
"/web/signup",
|
||||
data={
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
},
|
||||
)
|
||||
|
||||
def test_05_web_auth_signup_invalid_qcontext(self):
|
||||
"""It should catch EndTestException on signup qcontext"""
|
||||
self.session = http.root.session_store.new()
|
||||
self.opener = Opener(self.env.cr)
|
||||
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
|
||||
|
||||
with mock.patch(
|
||||
"odoo.addons.auth_signup.controllers.main.AuthSignupHome.get_auth_signup_qcontext"
|
||||
) as qcontext:
|
||||
qcontext.side_effect = EndTestException
|
||||
with self.assertRaises(HTTPError):
|
||||
# Catch HTTPError: 400 Client Error: BAD REQUEST
|
||||
self.signup("jackoneill", "!asdQWE12345_3")
|
||||
|
||||
def test_06_web_auth_signup_invalid_render(self):
|
||||
"""It should render & return signup form on invalid"""
|
||||
self.session = http.root.session_store.new()
|
||||
self.opener = Opener(self.env.cr)
|
||||
self.opener.cookies.set("session_id", self.session.sid, domain=HOST, path="/")
|
||||
|
||||
with mock.patch("odoo.http.db_filter") as db_filter:
|
||||
db_filter.side_effect = lambda dbs, host=None: [get_db_name()]
|
||||
# Signup: no name or partner given for new user
|
||||
response = self.url_open(
|
||||
"/web/signup",
|
||||
data={
|
||||
"login": "test@test.com",
|
||||
"password": "!asdQWE12345_7",
|
||||
"confirm_password": "!asdQWE12345_7",
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
},
|
||||
)
|
||||
|
||||
# Ensure we stay in the signup page
|
||||
self.assertEqual(response.request.path_url, "/web/signup")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(
|
||||
"Signup: no name or partner given for new user",
|
||||
response.text,
|
||||
)
|
||||
self.assertIn("X-Frame-Options", response.headers)
|
||||
self.assertEqual(response.headers["X-Frame-Options"], "SAMEORIGIN")
|
||||
|
||||
self.assertIn("Content-Security-Policy", response.headers)
|
||||
self.assertEqual(
|
||||
response.headers["Content-Security-Policy"], "frame-ancestors 'self'"
|
||||
)
|
||||
|
||||
def test_07_cloned_user_password_write_date(self):
|
||||
"""Users that are cloned should have their password_write_date updated"""
|
||||
partner = self.env["res.partner"].create({"name": "test partner"})
|
||||
vals = {
|
||||
"name": "Test User",
|
||||
"login": "test_user",
|
||||
"email": "test_user@odoo.com",
|
||||
"password": "Test_user_password123$",
|
||||
"partner_id": partner.id,
|
||||
}
|
||||
with freeze_time("2020-01-01"):
|
||||
self.env["res.users"].signup(vals)
|
||||
|
||||
original_user = self.env["res.users"].search([("login", "=", "test_user")])
|
||||
copied_user = original_user.copy()
|
||||
|
||||
self.assertTrue(
|
||||
copied_user.password_write_date > original_user.password_write_date
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue