Skip to content

Fixed #357 - Password hashing when changed manually in admin#360

Open
Alien501 wants to merge 8 commits intodjangoindia:mainfrom
Alien501:issue_357
Open

Fixed #357 - Password hashing when changed manually in admin#360
Alien501 wants to merge 8 commits intodjangoindia:mainfrom
Alien501:issue_357

Conversation

@Alien501
Copy link
Copy Markdown

@Alien501 Alien501 commented Apr 13, 2025

Closes #357

Now password will be hashed when modified in admin

Changes

  • Added hash function in users model

Type of change

  • Bug fix
  • Feature update
  • Breaking change
  • Documentation update

How has this been tested?

  • After changing password, a login attempt had been done.

Author Checklist

  • Code has been commented, particularly in hard-to-understand areas
  • Changes generate no new warnings
  • Vital changes have been captured in unit and/or integration tests
  • New and existing unit tests pass locally with my changes
  • Documentation has been extended, if necessary
  • Merging to main from fork:branchname

Summary by CodeRabbit

  • Bug Fixes
    • Passwords set or updated through the admin are now always securely hashed, both on user creation and when changing the password field. This prevents accidental plain-text storage and ensures consistent security across admin actions. No interface changes; existing users are unaffected. Future creates/edits via the admin work as before with improved safety.

@Alien501 Alien501 requested a review from ankanchanda as a code owner April 13, 2025 08:13
def save(self, *args, **kwargs):
self.email = self.email.lower().strip()

if not self.password.startswith('pbkdf2_sha256'):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would only work if the default hashing algorithm is used. If other algorithms are used, it would cause issues.

@Alien501
Copy link
Copy Markdown
Author

@PankajVishw50 made it to be generic, could you check my latest push

Copy link
Copy Markdown
Contributor

@PankajVishw50 PankajVishw50 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Just a small typo in comment has should be written hash

def save(self, *args, **kwargs):
self.email = self.email.lower().strip()
try:
# Trying to find the has used on the password, if it couldn't find a one, exception will be thrown, ie, password isn't hashed :-)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix Typo. has -> hash

try:
# Trying to find the hash used on the password, if it couldn't find a one, exception will be thrown, ie, password isn't hashed :-)
identify_hasher(self.password)
except:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not ideal to use bare except. You can use except Exception: if you're not sure what errors will be thrown.

ankanchanda
ankanchanda previously approved these changes May 10, 2025
Copy link
Copy Markdown
Collaborator

@ankanchanda ankanchanda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address the comment by sumit4613, and close this.

Comment on lines +118 to +120
if not self.password.startswith('pbkdf2_sha256'):
self.password = make_password(self.password)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to change anything on the login part?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No

Comment on lines +117 to +122
try:
# Trying to find the hash used on the password, if it couldn't find a one, exception will be thrown, ie, password isn't hashed :-)
identify_hasher(self.password)
except ValueError:
# Hashing password
self.password = make_password(self.password)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Alien501 I think this is not the best approach. This will try to hash the password everytime a user instance is saved. This is a django admin specific issue, and IMHO it would be better to override the save_model method of UserAdmin to something like:

def save_model(self, request, obj, form, change):
  if "password" in form.changed_data or not change:
      obj.set_password(form.cleaned_data["password"])
  return super().save_model(request, obj, form, change)

Now, whenever a user password is changed or a new user is created from django admin, It will hash its password. Also, by default django uses PBKDF2PasswordHasher which is pretty safe. Since we won't be changing the password hasher ever, using set_password(which uses make_password under the hood) looks like a good choice to me.

Let me know what you think.

cc: @ankanchanda @sumit4613

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I'll move the password hashing logic to UserAdmin instead. Thanks for the suggestion

@yanurag-dev
Copy link
Copy Markdown
Contributor

@CodeRabbit fullreview

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 28, 2025

@Yadavanurag13 I'll conduct a full review of the PR to ensure the password hashing implementation is properly done and follows best practices.

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 28, 2025

Walkthrough

Introduces a custom save_model in UserAdmin to hash passwords on creation or when the password field changes, calling set_password before delegating to the superclass. Also adds hashers imports in the user model file without changing public APIs.

Changes

Cohort / File(s) Summary
Admin password hashing flow
backend/djangoindia/db/admin.py
Adds UserAdmin.save_model override to hash passwords on create or when "password" is modified, using obj.set_password, then calls super().save_model.
Auth hashers import
backend/djangoindia/db/models/user.py
Adds imports for make_password and identify_hasher; no functional changes in methods. Minor formatting adjustment.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Admin as Admin User
    participant UI as Django Admin UI
    participant UA as UserAdmin.save_model
    participant User as User instance
    participant DB as Database

    Admin->>UI: Edit user password and save
    UI->>UA: save_model(request, obj, form, change)
    alt New object or "password" in changed_data
        UA->>User: set_password(raw_password)
        note right of UA: Hashes the provided password
    else No password change
        UA->>User: Leave password as-is
    end
    UA->>DB: super().save_model(...)\n(persist user)
    DB-->>UA: Saved
    UA-->>UI: Return
    UI-->>Admin: Save complete
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A bunny taps the admin screen—click, hop, hash!
No plaintext carrots left in the stash.
With salted twists and peppered might,
The password burrows out of sight.
Bugs beware my thumping tread—
Now only hashes get to bed. 🥕🔐

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The added imports of make_password and identify_hasher in the user model file are not used by the new admin save_model logic and do not relate to the specified bug fix, indicating an out-of-scope change beyond the linked issue’s objectives. Remove the unused imports from backend/djangoindia/db/models/user.py or clarify their intended use in the admin hashing workflow.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and succinctly describes the core change—ensuring passwords are hashed when modified manually via the admin—and references the linked issue for context without extraneous detail.
Linked Issues Check ✅ Passed The implementation of a custom save_model in UserAdmin now hashes passwords on creation and when the password field is modified via the admin, directly fulfilling the primary requirement of issue #357 without introducing unintended behavior.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eabde9f and eb1f093.

📒 Files selected for processing (2)
  • backend/djangoindia/db/admin.py (1 hunks)
  • backend/djangoindia/db/models/user.py (1 hunks)

Comment on lines +404 to +406
if "password" in form.changed_data or not change:
obj.set_password(form.cleaned_data["password"])
return super().save_model(request, obj, form, change)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid double-hashing already encoded passwords

Right now any value that looks like a Django hash (e.g. pbkdf2_sha256$...) gets passed back through set_password, so if an admin pastes an existing hash when migrating data the user’s credentials become unusable. Use the identify_hasher helper you already imported to detect pre-hashed inputs and only call set_password when we truly have raw text.

-        if "password" in form.changed_data or not change:
-            obj.set_password(form.cleaned_data["password"])
+        if "password" in form.changed_data or not change:
+            password = form.cleaned_data.get("password")
+            if password:
+                try:
+                    identify_hasher(password)
+                except ValueError:
+                    obj.set_password(password)
+                else:
+                    obj.password = password
         return super().save_model(request, obj, form, change)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if "password" in form.changed_data or not change:
obj.set_password(form.cleaned_data["password"])
return super().save_model(request, obj, form, change)
if "password" in form.changed_data or not change:
password = form.cleaned_data.get("password")
if password:
try:
identify_hasher(password)
except ValueError:
obj.set_password(password)
else:
obj.password = password
return super().save_model(request, obj, form, change)
🤖 Prompt for AI Agents
In backend/djangoindia/db/admin.py around lines 404-406, the code always calls
obj.set_password(...) which will double-hash values that are already Django
password hashes; use identify_hasher to detect pre-hashed inputs and only call
set_password for raw passwords. Modify the conditional so that when "password"
in form.changed_data or not change, you get pw = form.cleaned_data["password"],
attempt to identify_hasher(pw) in a try/except: if identification succeeds treat
it as an existing hash and assign obj.password = pw directly, otherwise call
obj.set_password(pw); keep the existing return super().save_model(...)
unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Password isn't hashed when changed in admin manually

6 participants