the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-access-engine-integration-r2/test_composed_cli.py
codex d9a74861ba
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Reject generated-only commits / generated-commit-guard (push) Waiting to run
docs: preserve reviewed NetKingdom session artifacts
2026-11-01 23:19:29 +01:00

99 lines
7.8 KiB
Python

import pathlib,subprocess,json,sqlite3,os,shlex,time
w=pathlib.Path('/tmp/custodian-forgejo-successor-attempt2/cli-work');s='/tmp/forgejo-private-input-r1';r='/tmp/forgejo-independent-r1'
fixtures=w/'fixtures'; fixtures.mkdir(exist_ok=True)
A='AbCdEf012345678-'*4;B='ZyXwVu_987654321'*4 # synthetic exact64
assert len(A)==64 and len(B)==64
for name,data,mode in [('good',A,0o400),('replacement',B,0o440),('newline',A+'\n',0o400),('short',A[:-1],0o400),('long',A+'A',0o400),('badbyte',A[:-1]+'=',0o400),('world',A,0o444),('writable',A,0o600),('executable',A,0o500),('empty','',0o400),('CLIENT_SECRET','',0o400)]:
p=fixtures/name
if p.exists():p.chmod(0o600)
p.write_text(data);p.chmod(mode)
for name in ['symlink']:
p=fixtures/name
if not p.exists():p.symlink_to('good')
if not (fixtures/'fifo').exists():os.mkfifo(fixtures/'fifo',0o400)
(fixtures/'directory').mkdir(exist_ok=True)
(fixtures/'badparent').mkdir(exist_ok=True);(fixtures/'badparent').chmod(0o777)
if (fixtures/'badparent/value').exists():(fixtures/'badparent/value').chmod(0o600)
(fixtures/'badparent/value').write_text(A);(fixtures/'badparent/value').chmod(0o400)
if not (fixtures/'parentlink').exists():(fixtures/'parentlink').symlink_to('directory',target_is_directory=True)
(w/'runtime').mkdir(exist_ok=True)
dbfile=w/'runtime/test.db'
if dbfile.exists():dbfile.unlink() # isolated synthetic test database only
ini=w/'runtime/app.ini';ini.write_text('''APP_NAME = synthetic-private-input-test
RUN_USER = worsch
WORK_PATH = /tmp/custodian-forgejo-successor-attempt2/cli-work/runtime
[security]
INSTALL_LOCK = true
SECRET_KEY = SYNTHETIC-TEST-KEY
[database]
DB_TYPE = sqlite3
SLOW_QUERY_THRESHOLD = 1ns
PATH = /tmp/custodian-forgejo-successor-attempt2/cli-work/runtime/test.db
[server]
APP_DATA_PATH = /tmp/custodian-forgejo-successor-attempt2/cli-work/runtime/data
OFFLINE_MODE = true
[repository]
ROOT = /tmp/custodian-forgejo-successor-attempt2/cli-work/runtime/repos
[log]
MODE = console
LEVEL = Error
''')
env={'PATH':'/usr/bin:/bin','HOME':str(w/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':str(w/'home/test.gitconfig'),'GIT_TERMINAL_PROMPT':'0','USER':'worsch','GITEA_WORK_DIR':str(w/'runtime')}
base=['bwrap','--unshare-pid','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--bind',str(w),str(w),'--dir','/run','--ro-bind',str(fixtures),'/run/forgejo-oidc','--ro-bind',str(fixtures/'good'),'/run/forgejo-oidc/CLIENT_SECRET','--bind',str(fixtures),'/run/writable-input','--chdir',str(w/'runtime'),'--','env','-i']+[k+'='+v for k,v in env.items()]+[str(w/'forgejo'),'--config',str(ini)]
results=[]
def run(args,ok=True,reason=None):
p=subprocess.run(base+args,capture_output=True,text=True,timeout=60); output=p.stdout+p.stderr
assert A not in output and B not in output, 'synthetic input leaked in output'
assert all(A not in arg and B not in arg for arg in base+args), 'synthetic input in argv'
assert all(A not in v and B not in v for v in env.values()), 'synthetic input in env'
results.append({'operation':args,'returncode':p.returncode,'stdout_stderr_secret_absent':True})
(w/'cli-attempts.json').write_text(json.dumps(results,indent=2))
if (p.returncode==0)!=ok:raise AssertionError('CLI unexpected status: '+repr(args)+'\n'+output[-5000:])
if reason:assert reason in output,output[-2000:]
return p
run(['migrate'])
clean_ini=ini.read_text()
ini.write_text(clean_ini.replace('DB_TYPE = sqlite3', 'DB_TYPE = sqlite3\nLOG_SQL = true'))
run(['admin','auth','add-oauth','--name','refused-debug-logging','--provider','github','--secret-file','/run/forgejo-oidc/CLIENT_SECRET'],False,'OAuth configuration requires SQL logging disabled')
ini.write_text(clean_ini)
def rows():
with sqlite3.connect(w/'runtime/test.db') as db:return db.execute('select id,name,cfg from login_source order by id').fetchall()
def cfg():return json.loads(rows()[0][2])
run(['admin','auth','add-oauth','--name','synthetic-oauth','--provider','github','--key','synthetic-public-id','--secret-file','/run/forgejo-oidc/CLIENT_SECRET','--group-claim-name','groups','--admin-group','unchanged-admin','--scopes','read:user,user:email'])
assert len(rows())==1 and cfg()['ClientSecret']==A
id=str(rows()[0][0]); before=cfg()
run(['admin','auth','update-oauth','--id',id,'--icon-url','https://synthetic.invalid/icon'])
assert cfg()['ClientSecret']==A
preserved=cfg()
run(['admin','auth','update-oauth','--id',id,'--secret-file','/run/forgejo-oidc/replacement'])
after=cfg();assert after['ClientSecret']==B
assert {k:v for k,v in after.items() if k!='ClientSecret'}=={k:v for k,v in preserved.items() if k!='ClientSecret'},'secret replacement changed unrelated config'
# Guarded inactive source exercises actual update omission and strict list semantics
# without discovery/network or claiming a browser login.
with sqlite3.connect(w/'runtime/test.db') as db:
guarded=after|{'Provider':'openidConnect','OpenIDConnectAutoDiscoveryURL':'https://synthetic.invalid/.well-known/openid-configuration','RequiredClaimName':'groups','RequiredClaimValues':['forge-admins','forge-coulomb-owners','forge-coulomb-readers']}
db.execute('update login_source set is_active=0,cfg=? where id=?',(json.dumps(guarded),id))
run(['admin','auth','update-oauth','--id',id,'--name','synthetic-guarded'])
assert cfg()['RequiredClaimValues']==guarded['RequiredClaimValues'] and cfg()['ClientSecret']==B
run(['admin','auth','update-oauth','--id',id,'--secret-file','/run/forgejo-oidc/good'])
assert cfg()['ClientSecret']==A and cfg()['RequiredClaimValues']==guarded['RequiredClaimValues']
for path in ['/run/forgejo-oidc/'+n for n in ['newline','short','long','badbyte','world','writable','executable','empty','symlink','fifo','directory','missing','badparent/value','parentlink/value']]+['relative','','/run/forgejo-oidc/../forgejo-oidc/good','/run/writable-input/good']:
before=rows()
for cmd in [['admin','auth','add-oauth','--name','refused','--provider','github'],['admin','auth','update-oauth','--id',id,'--name','refused']]:
run(cmd+['--secret-file',path],False,'secret-file input refused')
assert rows()==before,'failure changed DB'
for action in [['add-oauth','--name','refused'],['update-oauth','--id',id]]:
before=rows();run(['admin','auth']+action+['--secret','','--secret-file','/run/forgejo-oidc/good'],False,'mutually exclusive');assert rows()==before
# Failure to clear/ambiguously replace gate keeps stored application bytes unchanged.
before=rows();run(['admin','auth','update-oauth','--id',id,'--required-claim-values',''],False);assert rows()==before
# Actual XORM error hook under a failing SQLite trigger must not disclose cfg.
before=rows()
with sqlite3.connect(w/'runtime/test.db') as database:
database.execute("create trigger synthetic_private_error before update on login_source begin select raise(abort, 'synthetic-db-failure'); end")
run(['admin','auth','update-oauth','--id',id,'--secret-file','/run/forgejo-oidc/replacement'],False,'OAuth source update failed')
assert rows()==before
with sqlite3.connect(w/'runtime/test.db') as database:database.execute('drop trigger synthetic_private_error')
# Legacy explicit secret remains supported; synthetic legacy string is not private input.
run(['admin','auth','update-oauth','--id',id,'--secret','synthetic-legacy-literal'])
assert cfg()['ClientSecret']=='synthetic-legacy-literal' and cfg()['RequiredClaimValues']==guarded['RequiredClaimValues']
(w/'cli-results.json').write_text(json.dumps({'passed':True,'subprocesses':len(results),'real_backend_cli_and_sqlite':True,'network':'disabled by bwrap','secret_inputs':'synthetic only','values_never_in_argv_env_output':True,'actual_browser_login':False,'actual_readonly_file_bind':True,'replacement_preserves_other_config':True,'unsafe_SQL_logging_refused':True,'actual_error_and_slow_hooks_no_leak':True},indent=2));print('CLI integrations passed:',len(results))