Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
239 lines
7.9 KiB
Python
239 lines
7.9 KiB
Python
import frappe
|
|
from frappe import _
|
|
from frappe.auth import LoginAttemptTracker
|
|
from frappe.rate_limiter import rate_limit
|
|
from frappe.utils.password import check_password, update_password
|
|
|
|
|
|
@frappe.whitelist()
|
|
@rate_limit(limit=5, seconds=300) # 5 attempts per 5 minutes per user/IP
|
|
def change_password(old_password: str, new_password: str):
|
|
"""
|
|
Change password for the current logged-in user.
|
|
Uses Frappe's LoginAttemptTracker for attempt counting/lockout, and rate_limit for API abuse protection.
|
|
"""
|
|
user = frappe.session.user
|
|
if user == "Guest":
|
|
frappe.throw(_("You must be logged in to change your password"), frappe.AuthenticationError)
|
|
|
|
tracker = LoginAttemptTracker(user)
|
|
if not tracker.is_user_allowed():
|
|
frappe.throw(_("Too many failed attempts. Please try again after some time."))
|
|
|
|
if old_password == new_password:
|
|
frappe.throw(
|
|
_("New password cannot be the same as current password. Please choose a different password.")
|
|
)
|
|
|
|
try:
|
|
check_password(user, old_password)
|
|
except frappe.AuthenticationError:
|
|
tracker.add_failure_attempt()
|
|
frappe.throw(_("Incorrect current password. Please try again."))
|
|
else:
|
|
tracker.add_success_attempt()
|
|
|
|
# Validate new password strength (server-side enforcement)
|
|
from frappe.core.doctype.user.user import test_password_strength
|
|
|
|
result = test_password_strength(new_password)
|
|
feedback = result.get("feedback", {})
|
|
if not feedback.get("password_policy_validation_passed", False):
|
|
suggestions = feedback.get("suggestions", [])
|
|
frappe.throw(_("Password is too weak. {0}").format(" ".join(suggestions) if suggestions else ""))
|
|
|
|
update_password(user=user, pwd=new_password, logout_all_sessions=False)
|
|
return _("Password Updated Successfully")
|
|
|
|
|
|
def needs_password_setup() -> bool:
|
|
"""Whether the session user has no password of their own and must set one.
|
|
|
|
True for users provisioned without a password — the Frappe Cloud site owner,
|
|
users created by a script — who are logged into a session they never
|
|
authenticated for and so have no way back in once it expires.
|
|
|
|
Users who sign in through SSO are excluded: having no password is the
|
|
correct state for them, not something to fix.
|
|
"""
|
|
user = frappe.session.user
|
|
|
|
# Checked first: it is the discriminating one, and it short-circuits the
|
|
# other queries for everyone who already has a password — which is almost
|
|
# everyone, on every CRM page load.
|
|
if has_password(user):
|
|
return False
|
|
|
|
if frappe.get_system_settings("disable_user_pass_login"):
|
|
return False
|
|
|
|
# Every user carries a `frappe` provider row (Frappe issues one on insert
|
|
# so the site can act as an identity provider), so only other providers
|
|
# mean the user actually signs in through SSO.
|
|
if frappe.db.exists(
|
|
"User Social Login",
|
|
{
|
|
"parenttype": "User",
|
|
"parent": user,
|
|
"provider": ["!=", "frappe"],
|
|
"userid": ["is", "set"],
|
|
},
|
|
):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def has_password(user: str) -> bool:
|
|
"""Whether a password is stored for the user.
|
|
|
|
User passwords are hashed rows in `__Auth` with `encrypted = 0`, so
|
|
`get_decrypted_password` (which only looks at encrypted rows) cannot answer
|
|
this — hence the direct query.
|
|
"""
|
|
Auth = frappe.qb.Table("__Auth")
|
|
|
|
return bool(
|
|
(
|
|
frappe.qb.from_(Auth)
|
|
.select(Auth.name)
|
|
.where(
|
|
(Auth.doctype == "User")
|
|
& (Auth.name == user)
|
|
& (Auth.fieldname == "password")
|
|
& (Auth.encrypted == 0)
|
|
)
|
|
.limit(1)
|
|
).run()
|
|
)
|
|
|
|
|
|
@frappe.whitelist()
|
|
def add_existing_users(users: str | list, role: str = "Sales User"):
|
|
"""
|
|
Add existing users to the CRM by assigning them a role (Sales User or Sales Manager).
|
|
:param users: List of user names to be added
|
|
"""
|
|
frappe.only_for(["System Manager", "Sales Manager"], True)
|
|
is_system_manager = "System Manager" in frappe.get_roles()
|
|
|
|
if role == "System Manager" and not is_system_manager:
|
|
frappe.throw(_("Only System Managers can assign the System Manager role"), frappe.PermissionError)
|
|
|
|
if role == "Sales Manager" and not is_system_manager:
|
|
frappe.throw(_("Only System Managers can assign the Sales Manager role"), frappe.PermissionError)
|
|
|
|
users = frappe.parse_json(users)
|
|
|
|
for user in users:
|
|
update_user_role(user, role)
|
|
|
|
|
|
@frappe.whitelist()
|
|
def update_user_role(user: str, new_role: str):
|
|
"""
|
|
Update the role of the user to Sales Manager, Sales User, or System Manager.
|
|
:param user: The name of the user
|
|
:param new_role: The new role to assign (Sales Manager or Sales User)
|
|
"""
|
|
|
|
frappe.only_for(["System Manager", "Sales Manager"], True)
|
|
is_system_manager = "System Manager" in frappe.get_roles()
|
|
|
|
if new_role not in ["System Manager", "Sales Manager", "Sales User"]:
|
|
frappe.throw(_("Cannot assign this role"))
|
|
|
|
user_doc = frappe.get_doc("User", user)
|
|
target_roles = [d.role for d in user_doc.roles]
|
|
target_is_system_manager = "System Manager" in target_roles
|
|
|
|
if new_role == "System Manager" and not is_system_manager:
|
|
frappe.throw(_("Only System Managers can assign the System Manager role"), frappe.PermissionError)
|
|
|
|
if target_is_system_manager and not is_system_manager:
|
|
frappe.throw(_("Only System Managers can modify other System Managers"), frappe.PermissionError)
|
|
|
|
if new_role == "Sales Manager" and not is_system_manager:
|
|
frappe.throw(_("Only System Managers can assign the Sales Manager role"), frappe.PermissionError)
|
|
|
|
if new_role == "System Manager":
|
|
user_doc.append_roles("System Manager", "Sales Manager", "Sales User")
|
|
user_doc.set("block_modules", [])
|
|
if new_role == "Sales Manager":
|
|
user_doc.append_roles("Sales Manager", "Sales User")
|
|
remove_roles(user_doc, "System Manager")
|
|
if new_role == "Sales User":
|
|
node = frappe.db.get_value(
|
|
"CRM Sales Hierarchy", {"user": user}, ["name", "reports_to"], as_dict=True
|
|
)
|
|
if node:
|
|
has_reports = frappe.db.exists("CRM Sales Hierarchy", {"reports_to": node.name})
|
|
if has_reports or not node.reports_to:
|
|
frappe.throw(
|
|
_("Remove this user from the sales hierarchy before changing their role to Sales User")
|
|
)
|
|
user_doc.append_roles("Sales User")
|
|
remove_roles(user_doc, "Sales Manager", "System Manager")
|
|
update_module_in_user(user_doc, "FCRM")
|
|
|
|
user_doc.save(ignore_permissions=True)
|
|
|
|
|
|
@frappe.whitelist()
|
|
def remove_crm_roles_from_user(user: str):
|
|
"""
|
|
Remove a user means removing Sales User & Sales Manager roles from the user.
|
|
:param user: The name of the user to be removed
|
|
"""
|
|
frappe.only_for(["System Manager", "Sales Manager"], True)
|
|
|
|
if user == frappe.session.user:
|
|
frappe.throw(_("You cannot remove yourself."), frappe.PermissionError)
|
|
|
|
user_doc = frappe.get_doc("User", user)
|
|
roles = [d.role for d in user_doc.roles]
|
|
|
|
current_user_is_system_manager = "System Manager" in frappe.get_roles()
|
|
|
|
if "System Manager" in roles and not current_user_is_system_manager:
|
|
frappe.throw(_("Only System Managers can modify other System Managers"), frappe.PermissionError)
|
|
|
|
if user_doc.get("role_profiles") or user_doc.get("role_profile_name"):
|
|
return frappe.throw(
|
|
_("User {0} cannot be removed as it has a Role Profile assigned to it.").format(user)
|
|
)
|
|
|
|
if "Sales User" in roles:
|
|
remove_roles(user_doc, "Sales User")
|
|
if "Sales Manager" in roles:
|
|
remove_roles(user_doc, "Sales Manager")
|
|
if "System Manager" in roles and current_user_is_system_manager:
|
|
remove_roles(user_doc, "System Manager")
|
|
update_module_in_user(user_doc, "FCRM")
|
|
|
|
user_doc.save(ignore_permissions=True)
|
|
|
|
node_name = frappe.db.get_value("CRM Sales Hierarchy", {"user": user}, "name")
|
|
if node_name:
|
|
frappe.delete_doc("CRM Sales Hierarchy", node_name, ignore_permissions=True)
|
|
|
|
frappe.msgprint(_("User {0} has been removed from CRM roles.").format(user))
|
|
|
|
|
|
def remove_roles(self, *roles):
|
|
existing_roles = {d.role: d for d in self.get("roles")}
|
|
for role in roles:
|
|
if role in existing_roles:
|
|
self.get("roles").remove(existing_roles[role])
|
|
|
|
|
|
def update_module_in_user(user, module):
|
|
block_modules = frappe.get_all(
|
|
"Module Def",
|
|
fields=["name as module"],
|
|
filters={"name": ["!=", module]},
|
|
)
|
|
|
|
if block_modules:
|
|
user.set("block_modules", block_modules)
|