Finish Persims consolidation

This commit is contained in:
matcha committed 2026-10-04 02:57:22 +00:00
1 parent 48c07c3c62
commit 84fa9e70e6
1 file changed
+195
+195
View File
@@ -0,0 +1,195 @@
"""Named-vault Juice. Only the pinned extension can submit derived PATs.
Actions variables hold an allowlisted, value-free operation ledger. There is no
secret GET, raw source credential input, MCP registration, or bearer fallback.
"""
import asyncio
from contextlib import closing
from datetime import datetime, timezone
import json
import os
import re
import sqlite3
from starlette.concurrency import run_in_threadpool
from starlette.responses import JSONResponse, Response
from . import gitea, human_vault
ORIGINS = {'https://teahub.xyz'}
SCOPES = {'read:repository', 'read:issue', 'write:issue'}
ID = re.compile(r'[0-9a-f]{32}')
USERNAME = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,63}')
GATE = asyncio.Lock()
FIELDS = {'id', 'source_id', 'source_origin', 'source_username', 'provider', 'scopes',
'scope_boundary', 'created_at', 'status', 'provider_status', 'target_status',
'repo_id', 'replaces'}
def scopes(value):
if (not isinstance(value, list) or not 1 <= len(value) <= 3 or
any(not isinstance(s, str) or s not in SCOPES for s in value) or
len(set(value)) != len(value) or {'read:issue', 'write:issue'} <= set(value)):
raise ValueError()
return sorted(value)
def safe(value, vault):
if (not isinstance(value, dict) or set(value) != FIELDS or
value['provider'] != 'gitea' or value['source_origin'] not in ORIGINS or
not ID.fullmatch(value['id']) or not ID.fullmatch(value['source_id']) or
not USERNAME.fullmatch(value['source_username']) or
value['repo_id'] != vault['id'] or
value['scope_boundary'] != 'provider-api-class-wide' or
value['status'] not in ('pending', 'active', 'revoked', 'partial-failure') or
value['provider_status'] not in ('unknown', 'active', 'revoked') or
value['target_status'] not in ('absent', 'unknown', 'stored') or
(value['replaces'] is not None and not ID.fullmatch(value['replaces']))):
raise ValueError()
scopes(value['scopes'])
datetime.fromisoformat(value['created_at'])
return {key: value[key] for key in FIELDS}
def variable(user, vault, item_id):
result = gitea.actions(user, vault, 'variables', 'PERSIM_JUICE_' + item_id)
return safe(json.loads(result['data']), vault)
def save(user, vault, record, method='PUT'):
gitea.actions(user, vault, 'variables', 'PERSIM_JUICE_' + record['id'], method,
{'value': json.dumps(safe(record, vault), separators=(',', ':'))})
def operate(user, vault, operation, data):
if operation == 'list':
rows = gitea.actions(user, vault, 'variables')
items = []
for row in rows:
if re.fullmatch(r'PERSIM_JUICE_[0-9A-F]{32}', row.get('name', '')):
item = safe(json.loads(row['data']), vault)
if row['name'] != 'PERSIM_JUICE_' + item['id'].upper():
raise ValueError()
items.append(item)
return {'items': items}
item_id = data.get('id')
if not isinstance(item_id, str) or not ID.fullmatch(item_id):
raise ValueError()
if operation == 'prepare':
if set(data) != {'target', 'id', 'source_id', 'source_username', 'scopes', 'replaces'}:
raise ValueError()
source_id = data['source_id']
if not isinstance(source_id, str) or not ID.fullmatch(source_id):
raise ValueError()
# Metadata only: never fetch or decrypt the source ciphertext here.
found, code = human_vault.Store(os.environ['PERSIMMONS_HUMAN_VAULT_DB']).operate(user['owner'], 'GET')
source = next((i for i in found['items'] if i['id'] == source_id), None)
if code != 200 or not source or source['origin'] not in ORIGINS:
raise ValueError()
if not isinstance(data['source_username'], str) or not USERNAME.fullmatch(data['source_username']):
raise ValueError()
if source.get('username') and source['username'] != data['source_username']:
raise ValueError()
replaces = data['replaces']
if replaces is not None:
if not isinstance(replaces, str) or not ID.fullmatch(replaces):
raise ValueError()
old = variable(user, vault, replaces)
if old['status'] != 'active' or old['source_id'] != source_id or old['scopes'] != scopes(data['scopes']):
raise ValueError()
record = dict(id=item_id, source_id=source_id, source_origin=source['origin'],
source_username=data['source_username'], provider='gitea', scopes=scopes(data['scopes']),
scope_boundary='provider-api-class-wide', created_at=datetime.now(timezone.utc).isoformat(),
status='pending', provider_status='unknown', target_status='absent', repo_id=vault['id'], replaces=replaces)
# POST conflicts instead of adopting/replacing a prior operation.
save(user, vault, record, 'POST')
return {'item': record}
record = variable(user, vault, item_id)
if operation == 'write':
if set(data) != {'target', 'id', 'pat'} or record['status'] != 'pending':
raise ValueError()
if not isinstance(data['pat'], str) or not re.fullmatch(r'[0-9a-f]{40}', data['pat']):
raise ValueError()
record.update(target_status='unknown', provider_status='active', status='partial-failure')
save(user, vault, record)
try:
gitea.actions(user, vault, 'secrets', 'PERSIM_JUICE_' + item_id, 'PUT', {'data': data['pat']})
finally:
data['pat'] = ''
record.update(target_status='stored', status='active')
save(user, vault, record)
elif operation in ('remove', 'cleanup'):
expected = {'target', 'id'} | ({'provider_status'} if operation == 'cleanup' else set())
if set(data) != expected or (operation == 'cleanup' and data['provider_status'] not in ('revoked', 'unknown')):
raise ValueError()
try:
gitea.actions(user, vault, 'secrets', 'PERSIM_JUICE_' + item_id, 'DELETE')
except gitea.Failure as error:
if error.code != 404:
raise
record['target_status'] = 'absent'
if operation == 'cleanup':
record['provider_status'] = data['provider_status']
record['status'] = 'revoked' if record['provider_status'] == 'revoked' else 'partial-failure'
save(user, vault, record)
else:
raise ValueError()
return {'item': record}
def endpoint(resolve, database):
async def handler(request):
operation = request.path_params['operation']
origin = request.headers.getlist('origin')
extension = origin == [human_vault.EXTENSION_ORIGIN]
page = origin == ['https://persimmons.app']
headers = {'Cache-Control': 'no-store', 'Vary': 'Origin'}
def reply(data, code=200):
return JSONResponse(data, code, headers=headers)
if (request.url.scheme != 'https' or request.url.query or request.headers.get('authorization') or
not (extension or (page and operation in ('list', 'remove')))):
return reply({'error': 'forbidden'}, 403)
if extension:
headers.update({'Access-Control-Allow-Origin': human_vault.EXTENSION_ORIGIN,
'Access-Control-Allow-Credentials': 'true'})
if request.method == 'OPTIONS':
requested = {h.strip().lower() for h in request.headers.get('access-control-request-headers', '').split(',') if h.strip()}
if not extension or request.headers.get('access-control-request-method') != 'POST' or not requested <= {'content-type', 'x-vault-owner'}:
return reply({'error': 'forbidden'}, 403)
return Response(status_code=204, headers={**headers, 'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': 'Content-Type, X-Vault-Owner'})
user = await run_in_threadpool(resolve, request)
if not user:
return reply({'error': 'unauthorized'}, 401)
if (request.headers.getlist('x-vault-owner') != [user['owner']] or
(page and request.headers.get('x-requested-with') != 'Persimmons')):
return reply({'error': 'owner mismatch'}, 403)
if request.headers.get('content-type') != 'application/json':
return reply({'error': 'JSON required'}, 415)
try:
raw = bytearray()
async for chunk in request.stream():
raw.extend(chunk)
if len(raw) > 4096:
raise ValueError()
data = json.loads(raw)
raw.clear()
target = data['target']
if not isinstance(target, dict) or set(target) != {'id', 'name'} or type(target['id']) is not int:
raise ValueError()
if gitea.repo_name(target['name']) != target['name']:
raise ValueError()
with closing(database()) as db:
row = db.execute('SELECT repo_id FROM named_vaults WHERE owner=? AND name=?', (user['owner'], target['name'])).fetchone()
if not row or row[0] != target['id']:
return reply({'error': 'target mismatch'}, 403)
if operation == 'list' and set(data) != {'target'}:
raise ValueError()
async with GATE:
payload = await run_in_threadpool(operate, user, target, operation, data)
return reply({'owner': user['owner'], **payload})
except (ValueError, KeyError, TypeError, AttributeError):
return reply({'error': 'Invalid Juice request or metadata.'}, 400)
except (gitea.Failure, sqlite3.Error, OSError):
# A failed mutation is deliberately ambiguous; extension compensates.
return reply({'error': 'Juice unavailable. Check operation status before retrying.'}, 503)
return handler