169 lines
9.4 KiB
Python
169 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Admit an exact private inventory, add hooks, and verify vetoed SSH pushes."""
|
|
import argparse
|
|
import datetime
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import shlex
|
|
|
|
HERE=Path(__file__).parent
|
|
s=importlib.util.spec_from_file_location('template',HERE/'activate_forge_template.py')
|
|
t=importlib.util.module_from_spec(s)
|
|
s.loader.exec_module(t)
|
|
|
|
|
|
def repo_path(row):
|
|
if row['owner']!='coulomb' or not re.fullmatch(r'[a-z0-9-]+',row['name']):
|
|
raise ValueError('scope-identity-refused')
|
|
return '/data/git/gitea-repositories/coulomb/'+row['name']+'.git'
|
|
|
|
|
|
def inspect(pod,rows,expected):
|
|
commands=['set -euo pipefail']
|
|
for row in rows:
|
|
commands += ['repo='+shlex.quote(repo_path(row)),
|
|
'printf "REPO '+str(row['id'])+'\\n"',
|
|
'test -d "$repo" && test ! -L "$repo" && test ! -L "$repo/hooks" && test ! -L "$repo/hooks/pre-receive.d"',
|
|
'test -z "$(find "$repo/hooks" -maxdepth 2 -type l -print -quit)"',
|
|
'find "$repo/hooks" -maxdepth 2 -type f ! -name "*.sample" -exec sha256sum {} \\;',
|
|
'test "$(git --git-dir="$repo" rev-parse --is-shallow-repository)" = false',
|
|
'test "$(git --git-dir="$repo" rev-parse --show-object-format)" = sha1',
|
|
'config=$(git --git-dir="$repo" config --local --includes --name-only --list)',
|
|
'if printf "%s\\n" "$config" | grep -Eiq "(^extensions.partialclone$|^remote\\..*\\.(promisor|partialclonefilter)$)"; then exit 21; fi',
|
|
'if git --git-dir="$repo" rev-parse --verify HEAD >/dev/null 2>&1; then git --git-dir="$repo" ls-tree -r --format="%(objectmode)" HEAD | awk \'$0=="160000" {bad=1} END {exit bad}\'; fi']
|
|
raw=t.native.execute(pod,'bash','-s',payload=('\n'.join(commands)+'\n').encode(),timeout=180).decode()
|
|
found={}; current=None; by_id={r['id']:r for r in rows}
|
|
for line in raw.splitlines():
|
|
if line.startswith('REPO '): current=int(line[5:]);found[current]={}
|
|
else:
|
|
digest,path=line.split(' ',1);prefix=repo_path(by_id[current])+'/hooks/'
|
|
if not path.startswith(prefix) or not re.fullmatch('[a-f0-9]{64}',digest):raise ValueError('unexpected-hook-output')
|
|
found[current][path[len(prefix):]]=digest
|
|
if set(found)!=set(by_id) or any(hooks!=expected for hooks in found.values()):
|
|
raise ValueError('hook-inventory-changed')
|
|
|
|
|
|
def install(pod,path,content,digest):
|
|
# Temporary executable is outside the dispatch directory. Hard-link creation
|
|
# atomically exposes a complete file and refuses any existing destination.
|
|
script='''set -eu
|
|
umask 077
|
|
target=$1
|
|
temporary=$2
|
|
test ! -e "$target" && test ! -L "$target"
|
|
test ! -e "$temporary" && test ! -L "$temporary"
|
|
set -C
|
|
cat > "$temporary"
|
|
test "$(sha256sum "$temporary" | cut -d ' ' -f 1)" = "$3"
|
|
chmod 755 "$temporary"
|
|
ln "$temporary" "$target"
|
|
'''
|
|
temporary=path.rsplit('/hooks/pre-receive.d/',1)[0]+'/.custodian-expand-'+Path(path).name
|
|
t.native.execute(pod,'sh','-c',script,'sh',path,temporary,digest,payload=content)
|
|
|
|
|
|
def remove_owned(pod,path,digest,require_marker=False):
|
|
marker=path.rsplit('/hooks/pre-receive.d/',1)[0]+'/.custodian-expand-'+Path(path).name
|
|
t.native.execute(pod,'sh','-c',
|
|
'set -eu; if test -e "$1" || test -L "$1"; then test -f "$1" && test ! -L "$1"; '
|
|
'test "$(sha256sum "$1" | cut -d " " -f 1)" = "$2"; '
|
|
'if test "$4" = yes; then test "$1" -ef "$3"; fi; rm -- "$1"; fi; '
|
|
'if test -e "$3" || test -L "$3"; then test -f "$3" && test ! -L "$3"; '
|
|
'test "$(sha256sum "$3" | cut -d " " -f 1)" = "$2"; rm -- "$3"; fi',
|
|
'sh',path,digest,marker,'yes' if require_marker else 'no')
|
|
|
|
|
|
def rollback(pod,rows,original,proposal,require_marker=True):
|
|
for row in rows:
|
|
for name,digest in [(original['hook_name'],original['hook_sha256']),
|
|
(proposal['veto_name'],proposal['veto_sha256'])]:
|
|
remove_owned(pod,repo_path(row)+'/hooks/pre-receive.d/'+name,digest,require_marker)
|
|
inspect(pod,rows,original['existing_hook_hashes'])
|
|
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--proposal',type=Path,required=True)
|
|
parser.add_argument('--inventory',type=Path,required=True)
|
|
parser.add_argument('--receipt',type=Path,required=True)
|
|
mode=parser.add_mutually_exclusive_group()
|
|
mode.add_argument('--apply',action='store_true')
|
|
mode.add_argument('--rollback',action='store_true')
|
|
args=parser.parse_args()
|
|
p=json.loads(args.proposal.read_text())
|
|
for file,digest in p['executor_hashes'].items():
|
|
if hashlib.sha256((HERE/file).read_bytes()).hexdigest()!=digest:raise ValueError('executor-changed')
|
|
if hashlib.sha256(args.inventory.read_bytes()).hexdigest()!=p['inventory_sha256']:raise ValueError('scope-changed')
|
|
inventory=json.loads(args.inventory.read_text());by_id={r['id']:r for r in inventory['repositories']}
|
|
rows=[by_id[i] for i in p['target_ids']]
|
|
if len(rows)!=p['target_count'] or len(set(p['target_ids']))!=len(rows):raise ValueError('scope-count-changed')
|
|
if any(r['profile']!='builtin-only' or not r['head_compatible'] for r in rows):raise ValueError('inadmissible-scope')
|
|
original=json.loads((HERE/'receive-hook-proposal.json').read_text())
|
|
pod=t.target_pod(t.deployment(),original)
|
|
if pod['pod_uid']!=p['pod_uid']:raise ValueError('pod-changed')
|
|
t.check_artifacts(pod)
|
|
for a in original['artifacts'][:2]:
|
|
if t.native.execute(pod,'stat','-c','%a %u %g',original['version_directory']+'/'+a['name']).decode().strip()!='755 1000 1000':
|
|
raise ValueError('artifact-custody-changed')
|
|
if args.rollback:
|
|
receipt=json.loads(args.receipt.read_text())
|
|
if receipt['proposal_sha256']!=hashlib.sha256(args.proposal.read_bytes()).hexdigest():raise ValueError('receipt-changed')
|
|
rollback(pod,[by_id[i] for i in receipt['attempted_ids']],original,p,
|
|
require_marker=receipt['result']!='verified')
|
|
receipt['rollback']='verified-original-profiles-restored'
|
|
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n');return
|
|
inspect(pod,rows,original['existing_hook_hashes'])
|
|
fixture=Path(p['fixture_key'])
|
|
if hashlib.sha256(fixture.read_bytes()).hexdigest()!=p['fixture_sha256']:raise ValueError('fixture-changed')
|
|
hook=(HERE/'receive-profile-hook').read_bytes();veto=(HERE/'receive-probe-veto-expansion').read_bytes()
|
|
assert hashlib.sha256(hook).hexdigest()==original['hook_sha256']
|
|
assert hashlib.sha256(veto).hexdigest()==p['veto_sha256']
|
|
receipt={'proposal_id':p['proposal_id'],'proposal_sha256':hashlib.sha256(args.proposal.read_bytes()).hexdigest(),
|
|
'pod_uid':pod['pod_uid'],'result':'preflight-verified','target_count':len(rows),'attempted_ids':[],
|
|
'verified_ids':[],'at':datetime.datetime.now(datetime.timezone.utc).isoformat()}
|
|
def save():args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
|
|
save()
|
|
if not args.apply:print(json.dumps(receipt));return
|
|
try:
|
|
receipt['result']='applying';save()
|
|
for row in rows:
|
|
receipt['attempted_ids'].append(row['id']);save()
|
|
path=repo_path(row)+'/hooks/pre-receive.d/'
|
|
# No sample ref can be accepted during its probe, even if scanning fails.
|
|
install(pod,path+p['veto_name'],veto,p['veto_sha256'])
|
|
install(pod,path+original['hook_name'],hook,original['hook_sha256'])
|
|
proof=dict(pod,repositories=[row['name']],fixture_key=str(fixture),fixture_sha256=p['fixture_sha256'],
|
|
probe_ref=p['probe_ref'],proposal_id=p['proposal_id'])
|
|
t.activation.verify_pushes(t.native,proof)
|
|
remove_owned(pod,path+p['veto_name'],p['veto_sha256'],require_marker=True)
|
|
expected=dict(original['existing_hook_hashes']);expected['pre-receive.d/'+original['hook_name']]=original['hook_sha256']
|
|
inspect(pod,[row],expected)
|
|
receipt['verified_ids'].append(row['id']);save()
|
|
if len(receipt['verified_ids'])%5==0:print('Verified scanner profiles:',len(receipt['verified_ids']),'of',len(rows),flush=True)
|
|
inspect(pod,rows,expected)
|
|
receipt.update(result='verified',clean_scanner_and_synthetic_refusal_under_veto=True,
|
|
no_probe_refs_or_quarantined_commits=True,vetoes_removed=True,
|
|
original_dispatchers_preserved=True)
|
|
save()
|
|
except Exception as error:
|
|
receipt.update(result='failed',failure=type(error).__name__);save()
|
|
try:
|
|
rollback(pod,[by_id[i] for i in receipt['attempted_ids']],original,p)
|
|
receipt['rollback']='verified-original-profiles-restored'
|
|
except Exception as recovery:
|
|
receipt['rollback']='unverified:'+type(recovery).__name__
|
|
save();raise SystemExit('Expansion failed; inspect value-free receipt and rollback status.')
|
|
# After successful acceptance, receipt-bound rollback can use exact hashes.
|
|
# Cleanup failure leaves only inert ownership links outside the hook directory.
|
|
for row in rows:
|
|
target=repo_path(row)+'/hooks/pre-receive.d/'+original['hook_name']
|
|
marker=repo_path(row)+'/.custodian-expand-'+original['hook_name']
|
|
t.native.execute(pod,'sh','-c','set -eu; test "$1" -ef "$2"; rm -- "$2"','sh',target,marker)
|
|
receipt['ownership_markers_removed']=True;save()
|
|
print('All exact target profiles verified.',flush=True)
|
|
|
|
|
|
if __name__=='__main__':main()
|