fix(tools): use top-level tea comment invocation and formalize --login passthrough (#865) #866

Open
jason.woltje wants to merge 15 commits from fix/865-tea-cli-comment-invocation into main
2 changed files with 251 additions and 3 deletions
Showing only changes of commit 044744339b - Show all commits

View File

@@ -612,6 +612,7 @@ get_gitea_token_for_login() {
[[ -f "$config_file" ]] || return 1
LOGIN_NAME="$login_name" REPO_HOST="$repo_host" python3 - "$config_file" <<'PY'
import datetime
import os
import re
import sys
@@ -671,6 +672,138 @@ def _implicit_nonstring(text):
)
# PyYAML 6.0.3 SafeConstructor timestamp regexp (yaml/constructor.py). The
# constructor RE-parses a timestamp-tagged scalar with THIS pattern and then
# builds a datetime.date/datetime, which raises ValueError for an out-of-range
# calendar field (e.g. month 99, hour 25). _IMPLICIT_TIMESTAMP (the RESOLVER
# pattern) is byte-identical to PyYAML's resolver, so anything it tags is also
# tagged by PyYAML and re-matched here.
_TIMESTAMP_CONSTRUCT = re.compile(
r"""^(?P<year>[0-9][0-9][0-9][0-9])
-(?P<month>[0-9][0-9]?)
-(?P<day>[0-9][0-9]?)
(?:(?:[Tt]|[ \t]+)
(?P<hour>[0-9][0-9]?)
:(?P<minute>[0-9][0-9])
:(?P<second>[0-9][0-9])
(?:\.(?P<fraction>[0-9]*))?
(?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
(?::(?P<tz_minute>[0-9][0-9]))?))?)?$""",
re.X,
)
def _int_constructible(text):
# Replicate PyYAML SafeConstructor.construct_yaml_int and report whether it
# would succeed. A resolver-tagged int whose radix body is empty after
# underscore removal (e.g. "0b_", "0x_", "0x__") makes int(base) raise, so
# PyYAML fails the WHOLE document -> the fallback must fail closed too.
value = text.replace("_", "")
if value[:1] in ("+", "-"):
value = value[1:]
if value == "0":
return True
try:
if value.startswith("0b"):
int(value[2:], 2)
elif value.startswith("0x"):
int(value[2:], 16)
elif value[:1] == "0":
int(value, 8)
elif ":" in value:
[int(part) for part in value.split(":")]
else:
int(value)
except ValueError:
return False
return True
def _float_constructible(text):
# Replicate PyYAML SafeConstructor.construct_yaml_float. Retained for the
# WHOLE-document invariant even though PyYAML's float-tagged set is always
# float()-constructible: the fallback's float RESOLVER pattern is a strict
# superset of PyYAML's (it also matches unsigned-exponent spellings PyYAML
# keeps as strings), and every such extra is likewise constructible, so this
# never fails closed where PyYAML would emit a token.
value = text.replace("_", "").lower()
if value[:1] in ("+", "-"):
value = value[1:]
if value in (".inf", ".nan"):
return True
try:
if ":" in value:
[float(part) for part in value.split(":")]
else:
float(value)
except ValueError:
return False
return True
def _timestamp_constructible(text):
# Replicate PyYAML SafeConstructor.construct_yaml_timestamp: build the same
# datetime.date/datetime and report whether it raises. Returns False for an
# out-of-range calendar field (month/day/hour/...), matching PyYAML's
# whole-document ValueError.
match = _TIMESTAMP_CONSTRUCT.match(text)
if not match:
return False
values = match.groupdict()
try:
year = int(values["year"])
month = int(values["month"])
day = int(values["day"])
if not values["hour"]:
datetime.date(year, month, day)
return True
hour = int(values["hour"])
minute = int(values["minute"])
second = int(values["second"])
fraction = 0
if values["fraction"]:
frac = values["fraction"][:6]
frac += "0" * (6 - len(frac))
fraction = int(frac)
tzinfo = None
if values["tz_sign"]:
tz_hour = int(values["tz_hour"])
tz_minute = int(values["tz_minute"] or 0)
delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute)
if values["tz_sign"] == "-":
delta = -delta
tzinfo = datetime.timezone(delta)
elif values["tz"]:
tzinfo = datetime.timezone.utc
datetime.datetime(
year, month, day, hour, minute, second, fraction, tzinfo=tzinfo
)
except (ValueError, OverflowError):
return False
return True
def _constructible(text):
# WHOLE-DOCUMENT INVARIANT: the fallback resolves the SAME login token as
# PyYAML safe_load or fails closed -- never less conservative -- INCLUDING
# when PyYAML raises a CONSTRUCTOR error anywhere in the document. A plain
# scalar can match a typed implicit resolver (int/float/timestamp) yet NOT be
# constructible (e.g. 2023-99-99, 0b_, 0x_); PyYAML then raises on the whole
# load and yields no token, so the fallback MUST fail closed for the whole
# document too. This returns True only when PyYAML's constructor would build
# the scalar (null/bool token sets are always constructible), else False so
# the caller fails closed. `text` is assumed to satisfy _implicit_nonstring.
if _IMPLICIT_NULL.match(text) or _IMPLICIT_BOOL.match(text):
return True
if _IMPLICIT_TIMESTAMP.match(text):
return _timestamp_constructible(text)
if _IMPLICIT_INT.match(text):
return _int_constructible(text)
if _IMPLICIT_FLOAT.match(text):
return _float_constructible(text)
return True
# Sentinel: "outside the supported subset -> fail closed". Distinct from a
# genuine null (None), which is a valid resolved value.
_FAIL = object()
@@ -724,14 +857,31 @@ def _scalar(raw):
value = value.strip()
if not value:
return None
if value[0] in _FLOW or value[0] in ("|", ">", "?", "@", "`", '"', "'"):
return _FAIL # flow / block-scalar / reserved / anchor / quote indicator
if value[0] in _FLOW or value[0] in ("|", ">", "?", "@", "`", '"', "'", "%", ","):
return _FAIL # flow / block-scalar / reserved / directive / anchor / quote
if value[0] in ("-", "?", ":") and (len(value) == 1 or value[1] in (" ", "\t")):
# A bare block indicator, not a plain scalar: '-'/'- ' opens a sequence
# entry, '?'/'? ' a complex mapping key, ':'/': ' a mapping value -- all
# illegal in a value position, where PyYAML raises a scanner error on the
# whole document. "-x"/"-1"/"?x"/":x" (indicator NOT followed by space)
# remain valid plain scalars and fall through. Fail closed on the bare
# indicator so the fallback never emits a token PyYAML would refuse.
return _FAIL
if any(ch in _FLOW for ch in value):
return _FAIL
if ": " in value or value.endswith(":") or "\t" in value:
return _FAIL # nested-mapping-in-scalar / ambiguous
if _implicit_nonstring(value):
return None # non-string implicit type -> null-equivalent for a token
# A plain scalar PyYAML would tag as a non-string (null/bool/int/float/
# timestamp). If PyYAML's CONSTRUCTOR would build it, the value is a
# non-string -> null-equivalent for a token field: return None and keep
# parsing (as before). But if it matches a typed implicit resolver yet is
# NOT constructible (e.g. 2023-99-99, 0b_, 0x_), PyYAML raises on the
# WHOLE document and yields no token, so the fallback MUST fail closed
# for the whole document too -> _FAIL (which the caller turns into _Bail).
if not _constructible(value):
return _FAIL
return None
return value

View File

@@ -621,4 +621,102 @@ trailing: 1
'
assert_token "end marker then more content fails closed" "" primary git.example
# 16. CONSTRUCTOR-VALIDITY / INVALID-INDICATOR: a plain scalar can match a typed
# implicit resolver (int/float/timestamp) yet be NON-constructible, or begin
# with an indicator a plain scalar may not start with. PyYAML then RAISES on
# the WHOLE document (constructor error / scanner error) and yields NO token,
# so the fallback must ALSO fail closed for the whole document -- even though
# the (unrelated) malformed key sits alongside an otherwise-valid logins
# block whose token is itself well-formed. A prior residual proved STRUCTURE
# and implicit TYPE but not constructor validity, so it ignored the malformed
# key and still emitted the valid login token (fail-open in the dangerous
# direction). assert_both_fail_closed asserts fallback == PyYAML == no token.
assert_both_fail_closed() {
local desc="$1" login="$2" host="$3" got
got=$(token_fallback "$login" "$host")
if [[ -n "$got" ]]; then
echo "FAIL fallback [$desc]: expected fail-closed, got a token" >&2
exit 1
fi
if [[ "$HAVE_PYYAML" == true ]]; then
got=$(token_pyyaml "$login" "$host")
if [[ -n "$got" ]]; then
echo "FAIL pyyaml [$desc]: expected PyYAML to also fail closed (raise/no token), got a token" >&2
exit 1
fi
fi
}
# write_bad_key_fixture: an unrelated root key carrying $1 as its plain scalar,
# followed by an otherwise-valid logins block whose token is well-formed.
write_bad_key_fixture() {
write_fixture "bad: $1
logins:
- name: primary
url: https://git.example
token: TOK_PLAIN
"
}
# Non-constructible TIMESTAMP-tagged scalars: match the resolver, but the
# calendar field is out of range so PyYAML's datetime construction raises.
write_bad_key_fixture '2023-99-99' # month 99 / day 99 invalid
assert_both_fail_closed "bad-date 2023-99-99 fails closed like PyYAML" primary git.example
write_bad_key_fixture '2023-13-01' # month 13 invalid
assert_both_fail_closed "bad-month 2023-13-01 fails closed like PyYAML" primary git.example
write_bad_key_fixture '2023-01-15T25:00:00' # hour 25 invalid
assert_both_fail_closed "bad-hour timestamp fails closed like PyYAML" primary git.example
# Non-constructible INT-tagged scalars: match the int resolver, but the radix
# body is empty after underscore removal so int(base) raises.
write_bad_key_fixture '0b_'
assert_both_fail_closed "empty-binary 0b_ fails closed like PyYAML" primary git.example
write_bad_key_fixture '0x_'
assert_both_fail_closed "empty-hex 0x_ fails closed like PyYAML" primary git.example
write_bad_key_fixture '0x__'
assert_both_fail_closed "empty-hex 0x__ (multi-underscore) fails closed" primary git.example
# Invalid plain-scalar INDICATOR forms: a plain scalar may not begin with '%'
# (directive) or ',' (flow) -- PyYAML raises a scanner/parser error on the whole
# document, so the fallback fails closed on the leading indicator.
write_bad_key_fixture '%broken'
assert_both_fail_closed "leading-%% directive indicator fails closed" primary git.example
write_bad_key_fixture ',bad'
assert_both_fail_closed "leading-comma flow indicator fails closed" primary git.example
# Bare block indicators in a value position ('-'/'- ', '?'/'? ', ':'/': '):
# PyYAML raises a scanner error on the whole document, so the fallback must fail
# closed rather than accept the indicator as a plain-scalar string.
write_bad_key_fixture '-'
assert_both_fail_closed "bare dash (seq indicator) fails closed" primary git.example
write_bad_key_fixture '- x'
assert_both_fail_closed "dash-space (seq entry) fails closed" primary git.example
write_bad_key_fixture '? key'
assert_both_fail_closed "question-space (complex key) fails closed" primary git.example
# ...but an indicator NOT followed by whitespace is a valid plain scalar string,
# so the token still resolves (no over-broad fail-close).
write_bad_key_fixture '-x'
assert_token "dash-not-space is a plain string, token resolves" "TOK_PLAIN" primary git.example
write_bad_key_fixture ':x'
assert_token "colon-not-space is a plain string, token resolves" "TOK_PLAIN" primary git.example
# NOT over-broad: a genuinely CONSTRUCTIBLE typed scalar (or a look-alike PyYAML
# keeps as a plain string) leaves the document valid, so BOTH still resolve the
# login token -- the fix must not fail closed on these.
write_bad_key_fixture '2023-01-15'
assert_token "valid date unrelated key still resolves token" "TOK_PLAIN" primary git.example
write_bad_key_fixture '2023-01-15 10:00:00'
assert_token "valid datetime unrelated key still resolves token" "TOK_PLAIN" primary git.example
# '0o_' is NOT matched by PyYAML's int resolver (YAML 1.1 octal is 0[0-7]+, not
# 0o...), so PyYAML keeps it a STRING and resolves the token; the fallback must
# agree (no spurious fail-close).
write_bad_key_fixture '0o_'
assert_token "0o_ is a plain string in PyYAML, token still resolves" "TOK_PLAIN" primary git.example
# '4.e8' matches the fallback's (superset) float pattern but PyYAML keeps it a
# string; either way it is constructible, so the token still resolves in both.
write_bad_key_fixture '4.e8'
assert_token "4.e8 float look-alike still resolves token" "TOK_PLAIN" primary git.example
# A valid radix int as an unrelated key must not fail closed.
write_bad_key_fixture '0x1f'
assert_token "valid hex int unrelated key still resolves token" "TOK_PLAIN" primary git.example
echo "Gitea login resolution regression harness passed"