Files
huntingclubroster/tests/security_capacity_http.py
T

74 lines
6.4 KiB
Python

"""Real HTTP MFA, recovery, privilege and session-expiration boundaries."""
import re
from application_harness import Application
app=Application();checks=0
def check(ok,message):
global checks
if not ok:raise AssertionError(message)
checks+=1;print('PASS:',message)
def sign_password(email='admin@example.test',password='Admin test password 123!'):
_,body,_=app.request()
return app.request(data={'action':'SignIn.login','csrf_token':app.csrf(body),'user_email':email,'user_password':password})
def security(body,code,password='Admin test password 123!',**extra):
return app.request(data={'action':'security','csrf_token':app.csrf(body),'mfa_code':code,'current_password':password,**extra})
def sid():return next(c.value for c in app.client.handlers[0].cookiejar if c.name=='HUNTINGCLUBROSTER')
def age(**values):
# Find the cookie processor without assuming handler ordering.
jar=next(h.cookiejar for h in app.client.handlers if hasattr(h,'cookiejar'))
cookie=next(c.value for c in jar if c.name=='HUNTINGCLUBROSTER')
app.fixture('sessionAge',cookie,str(values.get('absolute','')),str(values.get('idle','')),str(values.get('recent','')))
try:
ids=app.fixture('tenantSeed')
status,body,_=sign_password()
check(status==200 and b'setup key' in body,'Password alone exposes setup and no administrator workspace')
token=app.csrf(body)
check(app.request(data={'action':'Users.deleteUser','csrf_token':token,'user_serial':str(ids['membera'])},ajax=True)[0]==403,'AJAX cannot bypass mandatory enrollment')
check(app.request('/main.php?Xaction=Roster.workspace&club_serial='+str(ids['clubA']))[1].find(b'setup key')>=0,'Direct club URLs cannot bypass enrollment')
check(app.request(data={'action':'security','mfa_code':'000000','current_password':'Admin test password 123!'})[0]==403,'MFA enrollment requires CSRF')
secret=re.search(rb'<pre>([A-Z2-7]+)</pre>',body).group(1).decode();code=app.totp(secret)
_,bad,_=security(body,code,'incorrect password')
check(b'Current password is incorrect' in bad and not app.fixture('mfaState','admin@example.test')['factor'],'Setup requires the current password and commits no partial factor')
status,body,_=security(bad,code)
codes=[c.decode() for c in re.findall(rb'\b[a-f0-9]{32}\b',body)]
check(status==200 and len(codes)==10 and b'Save your recovery codes' in body,'Valid setup shows ten recovery codes once')
state=app.fixture('mfaState','admin@example.test')
check(secret not in state['factor']['secret_cipher'] and all(c not in state['recovery'] for c in codes),'Database stores encrypted secret and hashed recovery codes')
_,body,_=app.request();app.token=app.csrf(body)
check(b'Save your recovery codes' not in body,'Recovery code plaintext is absent on subsequent pages')
app.logout();_,body,_=sign_password()
check(b'setup key' not in body and b'Authentication code' in body,'Enrolled administrator must supply a second factor on a new login')
_,body,_=security(body,code)
check(b'Password or authentication code is incorrect' in body,'An authenticator code already used during setup cannot be replayed')
_,body,_=security(body,codes[0]);app.token=app.csrf(body)
check(b'Club workspace' in body and len(app.fixture('mfaState','admin@example.test')['recovery'])==9,'Recovery code completes login and is consumed')
app.logout();_,body,_=sign_password();_,body,_=security(body,codes[0])
check(b'Password or authentication code is incorrect' in body,'Consumed recovery code cannot be reused')
_,body,_=security(body,codes[1]);app.token=app.csrf(body)
age(recent=301)
check(app.post('Users.changePassword')[0]==403,'Administrator password changes also require recent second-factor confirmation')
check(app.post('Roster.workspace',step='member',club_serial=ids['clubA'],membership_serial=ids['clubA_shared'],membership_status='active',joined_on='2026-01-01',membership_role='administrator')[0]==403,'Privilege changes reject stale recent authentication')
_,body,_=app.request('/main.php?Xaction=security');_,body,_=security(body,codes[2]);app.token=app.csrf(body)
check(app.post('Roster.workspace',step='member',club_serial=ids['clubA'],membership_serial=ids['clubA_shared'],membership_status='active',joined_on='2026-01-01',membership_role='administrator')[0]==200,'Privilege change succeeds after fresh password and second factor')
age(absolute=28800,idle=0)
check(app.post('toggleTheme')[0]==401,'Active session still expires at the eight-hour absolute boundary')
_,body,_=sign_password();_,body,_=security(body,codes[3]);app.token=app.csrf(body);age(absolute=28799,idle=1801)
check(app.post('toggleTheme')[0]==401,'Idle timeout remains enforced independently')
_,body,_=sign_password();_,body,_=security(body,codes[4]);app.token=app.csrf(body)
_,body,_=app.request('/main.php?Xaction=security');_,body,_=security(body,codes[5],factor_action='replace')
check(b'setup key' in body and app.fixture('mfaState','admin@example.test')['factor'] and len(app.fixture('mfaState','admin@example.test')['recovery'])==4,'Pending replacement preserves the old factor and remaining recovery codes')
check(b'Club workspace' in app.request('/main.php?Xaction=Roster.workspace')[1],'Authorized session retains its existing MFA access during replacement')
newsecret=re.search(rb'<pre>([A-Z2-7]+)</pre>',body).group(1).decode()
check(newsecret!=secret,'Replacement generates a fresh authenticator secret')
status,limited,_=security(body,app.totp(newsecret),factor_action='confirmReplacement')
check(status==429 and b'Too many authentication attempts' in limited,'Repeated MFA attempts enforce a persistent account budget')
app.fixture('clearTestAttempts')
_,body,_=security(limited,app.totp(newsecret),factor_action='confirmReplacement');check(b'Save your recovery codes' in body,'Replacement authenticator can be enrolled with fresh recovery codes')
_,body,_=app.request();app.token=app.csrf(body);app.logout();app.login('membera@example.test','Tenant test password 123!')
check(b'Club workspace' in app.request()[1],'Ordinary unenrolled members retain password login')
app.logout();_,body,_=sign_password('shared@example.test','Tenant test password 123!')
check(b'setup key' in body,'Newly promoted administrator must enroll despite an ordinary global account role')
print(checks,'security capacity HTTP checks passed.')
except Exception:
app.log.seek(0);print(app.log.read().decode(errors='replace'));raise
finally:app.close()