Improve unit test for hashed passwords

This commit is contained in:
Jay Lee
2026-07-04 08:20:56 -04:00
parent 69d886af4e
commit 123c34cc2b
2 changed files with 70 additions and 11 deletions

View File

@@ -15,14 +15,10 @@ from gamlib import gluprop as UProp
import base64 import base64
import time import time
from gamlib import glaction
from gamlib import glapi as API from gamlib import glapi as API
from gamlib import glcfg as GC from gamlib import glcfg as GC
from gamlib import glclargs
from gamlib import glentity
from gamlib import glgapi as GAPI from gamlib import glgapi as GAPI
from gamlib import glglobals as GM from gamlib import glglobals as GM
from gamlib import glindent
from gamlib import glmsgs as Msg from gamlib import glmsgs as Msg
from gamlib import glskus as SKU from gamlib import glskus as SKU
from gam.util.access import accessErrorExit, duplicateAliasGroupUserWarning, entityUnknownWarning from gam.util.access import accessErrorExit, duplicateAliasGroupUserWarning, entityUnknownWarning
@@ -96,11 +92,7 @@ from gam.util.tags import (
sendCreateUpdateUserNotification, sendCreateUpdateUserNotification,
) )
Act = glaction.GamAction() from gam.var import Act, Cmd, Ent, Ind
Ent = glentity.GamEntity()
Ind = glindent.GamIndent()
Cmd = glclargs.GamCLArgs()
from secrets import SystemRandom from secrets import SystemRandom
from passlib.hash import sha512_crypt from passlib.hash import sha512_crypt
@@ -108,6 +100,14 @@ UTF8 = 'utf-8'
UNKNOWN = 'Unknown' UNKNOWN = 'Unknown'
def hashPassword(password):
"""Hash a password using SHA-512 crypt for Google's API.
Returns a tuple of (hashed_password, hash_function_name).
The hash_function_name is always 'crypt' for Google's Directory API.
"""
return (sha512_crypt.hash(password, rounds=10000), 'crypt')
def _getGroupOrgUnitMap(): def _getGroupOrgUnitMap():
def getKeyFieldInfo(keyword, defaultField): def getKeyFieldInfo(keyword, defaultField):
@@ -255,8 +255,7 @@ class PasswordOptions():
if not self.notifyPasswordSet: if not self.notifyPasswordSet:
notify[up] = body[up] if self.clearPassword else Msg.CONTACT_ADMINISTRATOR_FOR_PASSWORD notify[up] = body[up] if self.clearPassword else Msg.CONTACT_ADMINISTRATOR_FOR_PASSWORD
if self.hashPassword: if self.hashPassword:
body[up] = sha512_crypt.hash(body[up], rounds=10000) body[up], body['hashFunction'] = hashPassword(body[up])
body['hashFunction'] = 'crypt'
elif self.b64DecryptPassword: elif self.b64DecryptPassword:
if body[up].lower()[:5] in ['{md5}', '{sha}']: if body[up].lower()[:5] in ['{md5}', '{sha}']:
body[up] = body[up][5:] body[up] = body[up][5:]

60
tests/test_password.py Normal file
View File

@@ -0,0 +1,60 @@
"""Unit tests for password hashing — verify GAM always produces SHA-512 crypt hashes.
Tests GAM's actual hashPassword() function from cmd/users/manage.py,
not passlib directly.
"""
import pytest
class TestPasswordHashing:
"""Ensure GAM's hashPassword() always generates SHA-512 crypt format hashes.
GAM sends hashed passwords to Google's Directory API with
hashFunction='crypt'. The hash MUST be SHA-512 ($6$ prefix),
never legacy formats like DES, MD5 ($1$), or SHA-256 ($5$).
"""
def test_hash_is_sha512_format(self):
"""Generated hash must start with $6$ (SHA-512 crypt identifier)."""
from gam.cmd.users.manage import hashPassword
hashed, func = hashPassword('test-password')
assert hashed.startswith('$6$'), (
f'Expected SHA-512 hash ($6$...) but got: {hashed[:10]}...'
)
def test_hash_function_is_crypt(self):
"""hashFunction returned must be 'crypt' for Google's API."""
from gam.cmd.users.manage import hashPassword
_, func = hashPassword('test-password')
assert func == 'crypt'
def test_hash_contains_rounds(self):
"""Generated hash must include the rounds parameter."""
from gam.cmd.users.manage import hashPassword
hashed, _ = hashPassword('test-password')
assert '$rounds=10000$' in hashed, (
f'Expected rounds=10000 in hash but got: {hashed[:30]}...'
)
def test_hash_verifies(self):
"""A generated hash must verify against its source password."""
from gam.cmd.users.manage import hashPassword
from passlib.hash import sha512_crypt
password = 'correct-horse-battery-staple'
hashed, _ = hashPassword(password)
assert sha512_crypt.verify(password, hashed)
def test_hash_rejects_wrong_password(self):
"""A generated hash must NOT verify against a different password."""
from gam.cmd.users.manage import hashPassword
from passlib.hash import sha512_crypt
hashed, _ = hashPassword('right-password')
assert not sha512_crypt.verify('wrong-password', hashed)
def test_different_passwords_produce_different_hashes(self):
"""Two different passwords must not produce the same hash."""
from gam.cmd.users.manage import hashPassword
h1, _ = hashPassword('password-one')
h2, _ = hashPassword('password-two')
assert h1 != h2