mirror of
https://github.com/GAM-team/GAM.git
synced 2026-07-04 12:51:36 +00:00
Improve unit test for hashed passwords
This commit is contained in:
@@ -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
60
tests/test_password.py
Normal 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
|
||||||
Reference in New Issue
Block a user