Add security-7-migration module: Spring Security 6.5.11 vs 7.1.2 jar diff, compile checks and runtime probes
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YPgGH6aMbSLANL3E2HHmS6
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the public API of Spring Security 6.5.11 against 7.1.2 straight from the jars.
|
||||
|
||||
Writes output/01-versions-and-baseline.txt and output/02-removed-api.txt.
|
||||
Needs a JDK (javap) on PATH or via JAVA_HOME, and access to Maven Central.
|
||||
"""
|
||||
import collections
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
JARS = ROOT / "target" / "jars"
|
||||
OUT = ROOT / "output"
|
||||
JAVAP = os.path.join(os.environ["JAVA_HOME"], "bin", "javap") if os.environ.get("JAVA_HOME") else "javap"
|
||||
|
||||
OLD, NEW = "6.5.11", "7.1.2"
|
||||
ARTIFACTS = [("security", a) for a in ["spring-security-core","spring-security-config","spring-security-web","spring-security-crypto","spring-security-oauth2-core","spring-security-oauth2-client","spring-security-oauth2-resource-server","spring-security-oauth2-jose","spring-security-test"]]
|
||||
|
||||
|
||||
# New 7.x artifacts that took over classes: a class missing from its old jar may simply have moved here.
|
||||
RELOCATION_TARGETS = ["spring-security-access", "spring-security-webauthn"]
|
||||
|
||||
|
||||
def version_for(group, which):
|
||||
return NEW if which == "new" else OLD
|
||||
|
||||
|
||||
def fetch(url, dest):
|
||||
if dest.exists():
|
||||
return
|
||||
for _ in range(8):
|
||||
try:
|
||||
with urllib.request.urlopen(url) as r:
|
||||
dest.write_bytes(r.read())
|
||||
return
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 429:
|
||||
raise
|
||||
time.sleep(4)
|
||||
raise RuntimeError("rate limited: " + url)
|
||||
|
||||
|
||||
def jar(group, artifact, which):
|
||||
v = version_for(group, which)
|
||||
dest = JARS / f"{artifact}-{v}.jar"
|
||||
fetch(f"https://repo1.maven.org/maven2/org/springframework/security/{artifact}/{v}/{artifact}-{v}.jar", dest)
|
||||
return dest
|
||||
|
||||
|
||||
def classes(path):
|
||||
with zipfile.ZipFile(path) as z:
|
||||
return sorted(n[:-6].replace("/", ".") for n in z.namelist()
|
||||
if n.endswith(".class") and not n.endswith("module-info.class") and "META-INF" not in n)
|
||||
|
||||
|
||||
def major(path):
|
||||
with zipfile.ZipFile(path) as z:
|
||||
name = next(n for n in z.namelist()
|
||||
if n.endswith(".class") and not n.endswith("module-info.class") and "META-INF" not in n)
|
||||
b = z.read(name)
|
||||
return int.from_bytes(b[6:8], "big")
|
||||
|
||||
|
||||
def norm(sig):
|
||||
"""Erase generics and the throws clause, so only a real change of name, parameters or return type counts."""
|
||||
sig = re.sub(r"\s+throws\s+.*$", "", sig)
|
||||
prev = None
|
||||
while prev != sig:
|
||||
prev = sig
|
||||
sig = re.sub(r"<[^<>]*>", "", sig)
|
||||
return " ".join(sig.split())
|
||||
|
||||
|
||||
MEMBER = re.compile(r"^ ((?:public|protected)[^;]*);$")
|
||||
|
||||
|
||||
def dump(path):
|
||||
"""One javap call per jar. Returns {class: (is_public, class_deprecated, {member signature: deprecated?})}."""
|
||||
names = [c for c in classes(path) if ".internal." not in c]
|
||||
result = {}
|
||||
# javap takes many class names at once; split the list to keep the command line short
|
||||
for i in range(0, len(names), 150):
|
||||
chunk = names[i:i + 150]
|
||||
p = subprocess.run([JAVAP, "-v", "-protected", "-cp", str(path)] + chunk, capture_output=True, text=True)
|
||||
cur_cls = None
|
||||
for block in re.split(r"(?m)^Classfile ", p.stdout)[1:]:
|
||||
lines = block.splitlines()
|
||||
header = lines[0]
|
||||
cls = re.search(r"!/(.*)\.class", header).group(1).replace("/", ".")
|
||||
public = False
|
||||
cls_dep = False
|
||||
members, cur, in_pool, seen_flags = {}, None, False, False
|
||||
for line in lines[1:]:
|
||||
if not seen_flags and line.startswith(" flags:"):
|
||||
public = "ACC_PUBLIC" in line
|
||||
seen_flags = True
|
||||
if line.startswith("Constant pool:"):
|
||||
in_pool = True
|
||||
continue
|
||||
if in_pool:
|
||||
if line.startswith("{"):
|
||||
in_pool = False
|
||||
continue
|
||||
m = MEMBER.match(line)
|
||||
if m:
|
||||
cur = " ".join(m.group(1).split())
|
||||
members[cur] = False
|
||||
continue
|
||||
if line.strip() == "Deprecated: true":
|
||||
if cur is None:
|
||||
cls_dep = True
|
||||
else:
|
||||
members[cur] = True
|
||||
if line.startswith("}"):
|
||||
cur = None
|
||||
if re.search(r"^Deprecated: true", block, re.M):
|
||||
cls_dep = True
|
||||
result[cls] = (public, cls_dep, members)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
JARS.mkdir(parents=True, exist_ok=True)
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
versions = [f"{'artifact':<34} {'6.x jar':<28} {'7.x jar':<28}"]
|
||||
moved_to = {}
|
||||
for art in RELOCATION_TARGETS:
|
||||
j = jar("security", art, "new")
|
||||
for c in classes(j):
|
||||
moved_to[c] = art
|
||||
removed_classes = collections.defaultdict(list)
|
||||
removed_members = collections.defaultdict(list)
|
||||
added = collections.Counter()
|
||||
for group, art in ARTIFACTS:
|
||||
old, new = jar(group, art, "old"), jar(group, art, "new")
|
||||
mo, mn = major(old), major(new)
|
||||
versions.append(f"{art:<34} {version_for(group, 'old')} (Java {mo - 44}){'':<9} {NEW} (Java {mn - 44})")
|
||||
do, dn = dump(old), dump(new)
|
||||
co, cn = set(classes(old)), set(classes(new))
|
||||
for c in sorted(co - cn):
|
||||
if c in do and do[c][0]:
|
||||
removed_classes[art].append((c, do[c][1]))
|
||||
added[art] = len(cn - co)
|
||||
for c in sorted(do):
|
||||
if c in dn and do[c][0]:
|
||||
new_norm = {norm(x) for x in dn[c][2]}
|
||||
for sig, dep in do[c][2].items():
|
||||
if norm(sig) not in new_norm:
|
||||
removed_members[art].append((c, sig, dep))
|
||||
(OUT / "01-versions-and-baseline.txt").write_text("\n".join(versions) + "\n")
|
||||
lines = []
|
||||
for _, art in ARTIFACTS:
|
||||
lines.append(f"== {art}: public classes removed: {len(removed_classes[art])}, "
|
||||
f"members removed from classes that remain: {len(removed_members[art])}, "
|
||||
f"new classes: {added[art]}")
|
||||
for c, dep in removed_classes[art]:
|
||||
note = f"; moved to {moved_to[c]}" if c in moved_to else "; gone"
|
||||
lines.append(f" class {c} [{'deprecated in 6.5' if dep else 'NOT deprecated in 6.5'}{note}]")
|
||||
for c, sig, dep in removed_members[art]:
|
||||
lines.append(f" member {c}: {sig} [{'deprecated in 6.5' if dep else 'NOT deprecated in 6.5'}]")
|
||||
moved = sum(1 for a in removed_classes for c, _ in removed_classes[a] if c in moved_to)
|
||||
total = sum(len(v) for v in removed_classes.values())
|
||||
lines.append(f"TOTAL public classes missing from the old jars: {total}, of which found again in {', '.join(RELOCATION_TARGETS)}: {moved}")
|
||||
(OUT / "02-removed-api.txt").write_text("\n".join(lines) + "\n")
|
||||
print("\n".join(versions))
|
||||
print("\n".join(l for l in lines if l.startswith("==")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compiles src/compat/*.java against Security 6.5.11 and 7.1.2 and records javac's answer in output/.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
mkdir -p target output
|
||||
for pair in "security6:6.5.11" "security7:7.1.2"; do
|
||||
profile=${pair%%:*}; v=${pair##*:}
|
||||
mvn -q -B dependency:build-classpath -P$profile -Dmdep.outputFile=target/cp-$v.txt >/dev/null || { echo "classpath failed for $v"; exit 1; }
|
||||
for f in OldSecurity NewSecurity NewManager; do
|
||||
out=output/$( [ $f = OldSecurity ] && echo 05-old-apis || { [ $f = NewSecurity ] && echo 06-fixed-apis || echo 07-new-manager; } )-$v.txt
|
||||
rm -rf target/classes-$f-$v; mkdir -p target/classes-$f-$v
|
||||
{
|
||||
echo "Spring Security $v, compiling $f.java"
|
||||
javac -Xlint:deprecation -proc:none -d target/classes-$f-$v -cp "$(cat target/cp-$v.txt)" src/compat/$f.java 2>&1 | grep -v "^Picked up"
|
||||
echo "javac exit status: ${PIPESTATUS[0]}"
|
||||
} > $out
|
||||
done
|
||||
done
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rewrites everything in output/. Needs JDK 17+, Maven 3.9 and internet access to Maven Central.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
python3 scripts/api_diff.py
|
||||
./scripts/compile-old-apis.sh
|
||||
./scripts/run-probes.sh
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the two probes on both Security lines; writes output/03-* and output/04-*.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
mkdir -p target output
|
||||
for pair in "security6:6.5.11" "security7:7.1.2"; do
|
||||
profile=${pair%%:*}; v=${pair##*:}
|
||||
mvn -q -B dependency:build-classpath -P$profile -Dmdep.outputFile=target/cp-$v.txt >/dev/null || exit 1
|
||||
rm -rf target/probe-$v; mkdir -p target/probe-$v
|
||||
javac -proc:none -d target/probe-$v -cp "$(cat target/cp-$v.txt)" src/probe/*.java 2>&1 | grep -v "^Picked up"
|
||||
java -cp "target/probe-$v:$(cat target/cp-$v.txt)" MatcherProbe 2>&1 | grep -vE "^Picked up|SpringSecurityCoreVersion|^INFO:" > output/03-matchers-$v.txt
|
||||
java -cp "target/probe-$v:$(cat target/cp-$v.txt)" MethodSecurityProbe 2>&1 | grep -vE "^Picked up|SpringSecurityCoreVersion|^INFO:|^WARNING:|AnnotationConfigApplicationContext refresh" > output/04-global-method-security-$v.txt
|
||||
done
|
||||
|
||||
# 7.1.2 again, with the spring-security-access jar that api_diff.py downloaded added to the class path
|
||||
ACC=target/jars/spring-security-access-7.1.2.jar
|
||||
if [ -f "$ACC" ]; then
|
||||
java -cp "target/probe-7.1.2:$ACC:$(cat target/cp-7.1.2.txt)" MethodSecurityProbe 2>&1 \
|
||||
| grep -vE "^Picked up|SpringSecurityCoreVersion|^INFO:|^WARNING:|AnnotationConfigApplicationContext refresh" > output/04-global-method-security-7.1.2-with-access.txt
|
||||
fi
|
||||
Reference in New Issue
Block a user