net-kingdom/tests/test_admin_bootstrap_privacyidea.py
tegwick 19a1c75d30
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix(security): make admin bootstrap private and non-overwriting
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a12d13-27f2-7163-b8c0-f682af02772c
2026-10-12 15:02:15 +02:00

123 lines
4.9 KiB
Python

"""Run the in-pod adapter against actual privacyIDEA 3.12.2 models on SQLite.
Install that exact package in a disposable environment. No native cluster or
production config is used; this does not replace deployed-image acceptance.
"""
import importlib.metadata
import importlib.util
import io
from pathlib import Path
import sys
import pytest
pytest.importorskip('privacyidea')
if importlib.metadata.version('privacyIDEA') != '3.12.2':
pytest.skip('adapter verification requires privacyIDEA 3.12.2', allow_module_level=True)
from flask import Flask
from privacyidea.models import Admin, Policy, db
from privacyidea.lib.crypto import hash_with_pepper
from privacyidea.lib.auth import verify_db_admin
import privacyidea.cli
ROOT=Path(__file__).resolve().parents[1]
PASSWORD='SyntheticAdminCredential12345678901234567890'
@pytest.fixture
def native(tmp_path,monkeypatch):
spec=importlib.util.spec_from_file_location('admin_remote_native',ROOT/'sso-mfa/k8s/privacyidea/bootstrap-admin-remote.py')
remote=importlib.util.module_from_spec(spec);spec.loader.exec_module(remote)
app=Flask('disposable-bootstrap')
app.config.update(SQLALCHEMY_DATABASE_URI='sqlite:///'+str(tmp_path/'db.sqlite'),
SQLALCHEMY_TRACK_MODIFICATIONS=False,PI_PEPPER='synthetic-pepper',SECRET_KEY='synthetic-key')
db.init_app(app)
with app.app_context(): db.create_all()
monkeypatch.setattr(privacyidea.cli,'create_silent_app',lambda:app)
def invoke(password=PASSWORD):
import json
monkeypatch.setattr(sys,'stdin',io.TextIOWrapper(io.BytesIO(json.dumps({'password':password}).encode())))
return remote.main()
import logging
previous_disable = logging.root.manager.disable
try:
yield app,remote,invoke
finally:
logging.disable(previous_disable)
def test_real_model_creation_and_repeat_preserve_password_hash(native):
app,_,invoke=native
assert invoke()=={'result':'created','mfa_verified':False}
with app.app_context():
before=Admin.query.filter_by(username='pi-admin').one().password
assert verify_db_admin('pi-admin',PASSWORD)
assert [a.username for a in Admin.query.all()]==['pi-admin']
assert Policy.query.one().adminuser=='pi-admin'
assert invoke()=={'result':'verified','mfa_verified':False}
with app.app_context(): assert Admin.query.one().password==before
def test_real_model_mismatch_never_resets_password(native):
app,remote,invoke=native
invoke()
with app.app_context(): before=Admin.query.one().password
with pytest.raises(remote.Refused):invoke('DifferentSyntheticPassword1234567890123456')
with app.app_context(): assert Admin.query.one().password==before
def test_real_legacy_broad_policy_refused_before_account_creation(native):
app,remote,invoke=native
with app.app_context():
db.session.add(Policy(name='pi-admin-all-rights',scope='admin',action='*'));db.session.commit()
with pytest.raises(remote.Refused):invoke()
with app.app_context():
assert Admin.query.count()==0
assert Policy.query.one().adminuser==''
def test_real_other_admin_requires_explicit_policy_migration(native):
app,remote,invoke=native
with app.app_context():
db.session.add(Admin(username='other-admin',password=hash_with_pepper(PASSWORD)));db.session.commit()
with pytest.raises(remote.Refused):invoke()
with app.app_context():
assert [a.username for a in Admin.query.all()]==['other-admin']
assert Policy.query.count()==0
def test_real_failed_policy_insert_rolls_back_account(native,monkeypatch):
app,remote,invoke=native
original=db.session.add
def fail_policy(obj,*args,**kwargs):
if isinstance(obj,Policy):raise RuntimeError('synthetic-failure')
return original(obj,*args,**kwargs)
monkeypatch.setattr(db.session,'add',fail_policy)
with pytest.raises(remote.Refused):invoke()
with app.app_context():
assert Admin.query.count()==0
assert Policy.query.count()==0
def test_real_concurrent_account_conflict_never_updates_existing_row(native,monkeypatch):
import privacyidea.lib.auth
app,remote,invoke=native
with app.app_context():
db.session.add(Admin(username='pi-admin',password=hash_with_pepper('ExistingSyntheticPassword1234567890123456')))
db.session.commit();before=Admin.query.one().password
# Simulate an account appearing after the initial absence observation.
monkeypatch.setattr(privacyidea.lib.auth,'list_db_admin',lambda:[])
with pytest.raises(remote.Refused):invoke()
with app.app_context():
assert Admin.query.one().password==before
assert Policy.query.count()==0
def test_real_external_admin_realm_requires_owner_profile(native):
app,remote,invoke=native
app.config['SUPERUSER_REALM']=['directory-admins']
with pytest.raises(remote.Refused):invoke()
with app.app_context():
assert Admin.query.count()==0
assert Policy.query.count()==0