Implement audited lost-factor recovery and track remaining P04 acceptance
All checks were successful
Authentication acceptance / acceptance (push) Successful in 58s
Authentication acceptance / provider-contract (push) Successful in 14s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
tegwick 2026-09-13 17:28:45 +02:00
parent 244e7e096f
commit 3b7df9047e
5 changed files with 229 additions and 1 deletions

View file

@ -13,6 +13,7 @@ def run():
with tempfile.TemporaryDirectory(prefix="provider-contract-") as d:
root=Path(d);(root/'enckey').write_bytes(os.urandom(96))
cfg=root/'fixture.cfg';cfg.write_text("SQLALCHEMY_DATABASE_URI='sqlite:///:memory:'\nSECRET_KEY='isolated-fixture-only'\nPI_PEPPER='isolated-fixture-only'\nPI_NO_RESPONSE_SIGN=True\nPI_AUDIT_NO_SIGN=True\nPI_LOGFILE="+repr(str(root/'log'))+"\nPI_ENCFILE="+repr(str(root/'enckey'))+"\nPI_TRUSTED_JWT=[]\n")
with cfg.open('a') as f:f.write('PI_AUDIT_SQL_URI='+repr('sqlite:///'+str(root/'audit.sqlite'))+'\n')
from privacyidea.app import create_app
from privacyidea.models import db
from privacyidea.lib.policy import set_policy,enable_policy
@ -24,6 +25,9 @@ def run():
phase="fixture_database"
with app.app_context():
check('database_isolated',str(db.engine.url)=='sqlite:///:memory:');db.create_all()
from privacyidea.lib.audit import getAudit
from privacyidea.lib.auditmodules.sqlaudit import LogEntry
LogEntry.__table__.create(getAudit(app.config).engine,checkfirst=True)
passwd=root/'users';passwd.write_text('alice:'+crypt_ctx.hash('fixture-password',scheme='sha512_crypt')+':1001:1001:Fixture:/tmp:/bin/false\n')
save_resolver({'resolver':'fixture-users','type':'passwdresolver','fileName':str(passwd)})
set_realm('fixture',[{'name':'fixture-users'}])
@ -85,6 +89,18 @@ def run():
time.sleep(3)
code,_=req('GET','/token/',token=short)
check('expired_provider_jwt_rejected',code==401)
phase='platform_admin_recovery'
from factor_recovery import ProviderStore,recover,RecoveryError
with app.app_context():
store=ProviderStore(app,realm='fixture')
request=dict(user='alice',serial=serial,realm='fixture',actor='fixture-platform-operator',reference='fixture-recovery-001')
preview=recover(store,request)
check('recovery_preview_preserves_factor',preview['active'] is True and preview['changes_applied'] is False)
approved=dict(request,apply=True,identity_verified=True,expected_version=preview['version'])
applied=recover(store,approved)
check('platform_recovery_disabled_factor',applied['status']=='recovered' and store.snapshot('alice',serial)['active'] is False)
check('platform_recovery_audit_durable',store.receipt(request['reference'])['complete'] is True)
check('platform_recovery_replay_no_mutation',recover(store,approved)['replayed'] is True)
phase='finished';result['success']=True
with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()):
try:run()