Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a12d13-27f2-7163-b8c0-f682af02772c
100 lines
4.2 KiB
Python
100 lines
4.2 KiB
Python
"""Version-scoped in-pod bootstrap. Password input is JSON on stdin only.
|
|
|
|
Existing credentials/policies are never updated. Insert-only DB writes and
|
|
unique constraints protect against concurrent creation. Native MFA/effective
|
|
policy acceptance is a separate attended procedure.
|
|
"""
|
|
import json
|
|
import logging
|
|
import re
|
|
import sys
|
|
|
|
NAME = 'pi-admin'
|
|
POLICY = 'pi-admin-all-rights'
|
|
|
|
|
|
class Refused(Exception):
|
|
pass
|
|
|
|
|
|
def admitted_policy(policy):
|
|
if (policy.get('name') != POLICY or policy.get('scope') != 'admin'
|
|
or policy.get('adminuser') != [NAME] or policy.get('action') != {'*': True}
|
|
or policy.get('active') is not True or policy.get('priority') != 1
|
|
or policy.get('check_all_resolvers') is not False
|
|
or policy.get('user_case_insensitive') is not False):
|
|
return False
|
|
return all(not policy.get(key) for key in (
|
|
'realm', 'adminrealm', 'resolver', 'pinode', 'user', 'client', 'time', 'conditions', 'user_agents'))
|
|
|
|
|
|
def ensure(password, admins, policies, verify, insert):
|
|
existing = NAME in admins
|
|
named = [policy for policy in policies if policy.get('name') == POLICY]
|
|
if len(named) > 1 or (named and not admitted_policy(named[0])): raise Refused()
|
|
if not named:
|
|
# Introducing the first admin policy changes defaults for every admin.
|
|
# Existing deployments need their owner's policy migration, not bootstrap.
|
|
if any(admin != NAME for admin in admins) or any(p.get('scope') == 'admin' for p in policies):
|
|
raise Refused()
|
|
if existing and not verify(NAME, password): raise Refused()
|
|
if not existing or not named:
|
|
insert(password, account=not existing, policy=not named)
|
|
if not verify(NAME, password): raise Refused()
|
|
return {'result': 'verified' if existing else 'created', 'mfa_verified': False}
|
|
|
|
|
|
def main():
|
|
from importlib.metadata import version
|
|
if version('privacyIDEA') != '3.12.2': raise Refused()
|
|
raw = sys.stdin.buffer.read(1025)
|
|
if len(raw) > 1024: raise Refused()
|
|
payload = json.loads(raw)
|
|
if (not isinstance(payload, dict) or set(payload) != {'password'}
|
|
or not isinstance(payload['password'], str)
|
|
or not re.fullmatch(r'[A-Za-z0-9]{32,128}', payload['password'])):
|
|
raise Refused()
|
|
# Avoid library diagnostics carrying credential/configuration data.
|
|
logging.disable(logging.CRITICAL)
|
|
from privacyidea.cli import create_silent_app
|
|
from privacyidea.lib.auth import list_db_admin, verify_db_admin
|
|
from privacyidea.lib.crypto import hash_with_pepper
|
|
from privacyidea.lib.policy import get_policies
|
|
from privacyidea.models import Admin, Policy, db
|
|
from privacyidea.models.config import save_config_timestamp
|
|
app = create_silent_app()
|
|
with app.app_context():
|
|
# Username-only bootstrap policy must not admit namesakes in other realms.
|
|
if app.config.get('SUPERUSER_REALM', []): raise Refused()
|
|
def insert(password, *, account, policy):
|
|
try:
|
|
# Do not call Admin.save/create_db_admin or set_policy: they upsert.
|
|
if account:
|
|
db.session.add(Admin(username=NAME, password=hash_with_pepper(password)))
|
|
if policy:
|
|
db.session.add(Policy(name=POLICY, scope='admin', action='*', adminuser=NAME))
|
|
db.session.flush() # constraint conflicts fail without updates
|
|
if policy: save_config_timestamp()
|
|
db.session.commit()
|
|
except Exception:
|
|
db.session.rollback()
|
|
raise Refused() from None
|
|
result = ensure(payload['password'], [admin.username for admin in list_db_admin()],
|
|
get_policies(), verify_db_admin, insert)
|
|
if not any(admitted_policy(p) for p in get_policies()): raise Refused()
|
|
return result
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# Only the fixed receipt may reach the caller; suppress all library output.
|
|
import os
|
|
receipt_fd = os.dup(1)
|
|
with open(os.devnull, 'w') as sink:
|
|
os.dup2(sink.fileno(), 1); os.dup2(sink.fileno(), 2)
|
|
try:
|
|
result = main()
|
|
os.write(receipt_fd, (json.dumps(result) + '\n').encode())
|
|
except Exception:
|
|
sys.exit(1)
|
|
finally:
|
|
os.close(receipt_fd)
|