#!/usr/bin/env python3
"""Prepare an offline private access inventory; never infer target identities or grants."""
import argparse
import hashlib
import html
import json
import os
import re
import stat
import sys
from collections import Counter
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from coulomb_social.apps.spaces.bubble_transfer import normalize_ref # noqa: E402
FIELDS = {
'spaces': {'Created By': False, 'crew_list_user': True,
'members_list_user': True, 'guests_list_user': True},
'articles': {'Created By': False, '_creator_user': False,
'_owner_user': False, 'moders_list_user': True},
}
OPTIONS = {'spaces': ['isprivate_boolean', 'addnewusersasmember_boolean',
'addnewusersasguest_boolean'],
'articles': ['access_option_visibility']}
def sha(raw):
return hashlib.sha256(raw).hexdigest()
def private_read(path):
if any(p.is_symlink() for p in (path, *path.parents)):
raise ValueError('source symlink')
info = path.stat()
if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid()
or info.st_mode & 0o077 or info.st_nlink != 1 or info.st_size > 16 * 1024**2):
raise ValueError('private bounded regular source required')
return path.read_bytes()
def reference(value):
try:
result = normalize_ref(value)
except (ValueError, TypeError):
return None
return result if re.fullmatch(r'[0-9]+x[0-9]+', result) else None
def field_state(row, field, many):
if field not in row:
return {'state': 'missing', 'principals': []}
raw = row[field]
if raw is None:
return {'state': 'null', 'raw': raw, 'principals': []}
if many and not isinstance(raw, list):
return {'state': 'invalid_container', 'raw': raw, 'principals': []}
if not many and not isinstance(raw, str):
return {'state': 'invalid_container', 'raw': raw, 'principals': []}
values = raw if many else [raw]
refs = [reference(v) for v in values]
return {'state': ('empty' if not values else 'unresolved_reference'
if None in refs else 'observed'),
'raw': raw, 'principals': sorted(set(r for r in refs if r is not None)),
'unresolved_count': sum(r is None for r in refs)}
def audit(inputs):
"""Source values remain private; the separate summary contains aggregate counts only."""
records = {}
principals = set()
summary = {}
for kind in FIELDS:
source = inputs[kind]
if not isinstance(source, dict) or any(not isinstance(v, dict) for v in source.values()):
raise ValueError('record dictionaries required')
records[kind] = []
counters = {f: Counter() for f in FIELDS[kind]}
for key, row in sorted(source.items()):
fields = {f: field_state(row, f, many) for f, many in FIELDS[kind].items()}
for f, value in fields.items():
counters[f][value['state']] += 1
principals.update(value['principals'])
records[kind].append({'source_id': key, 'fields': fields,
'options': {f: {'present': f in row, 'value': row.get(f)}
for f in OPTIONS[kind]}})
summary[kind] = {
'records': len(source), 'field_states': counters,
'option_fields': {f: {
'present': sum(f in row for row in source.values()),
'missing': sum(f not in row for row in source.values()),
'distinct_observed_values': len({json.dumps(row[f], sort_keys=True)
for row in source.values() if f in row}),
} for f in OPTIONS[kind]},
}
retained = inputs['retained_spaces']
if not isinstance(retained, dict):
raise ValueError('retained inventory required')
known = set(retained) | set(inputs['spaces'])
summary.update({'known_spaces': len(known), 'fresh_space_records': len(inputs['spaces']),
'inventory_only_spaces': len(set(retained) - set(inputs['spaces'])),
'distinct_source_principals': len(principals),
'target_identity_mappings_established': 0,
'target_grants_proposed': 0, 'production_changed': False,
'source_visibility_semantics_verified': False})
return {'schema': 'coulomb.bubble-access-inventory.v1', 'summary': summary,
'source_principals': sorted(principals), 'records': records,
'inventory_only_space_ids': sorted(set(retained) - set(inputs['spaces'])),
'limitations': [
'Missing fields are unknown, not empty grants.',
'Source roles and visibility are observed values, not target permissions.',
'Existing one-space owner admission does not map every source identity.',
'This capture does not prove complete or current provider membership.',
'No target identity, role mapping or import manifest is created.']}
def review_html(report):
rows = []
for kind, records in report['records'].items():
for row in records:
data = json.dumps(row, ensure_ascii=False, indent=2)
rows.append('' + html.escape(kind + ': ' + row['source_id'])
+ '
' + html.escape(data) + '
Observed source metadata only. ' 'No target identities or grants have been inferred.
'
+ html.escape(json.dumps(report['summary'], indent=2)) + '