mirror of
https://github.com/d9pouces/id_configurator.git
synced 2026-11-09 01:47:21 +01:00
586f091eeb
Signed-off-by: d9pouces <github@19pouces.net>
301 lines
11 KiB
Python
301 lines
11 KiB
Python
"""Add the configured root certificates to every Java trust store detected
|
|
under `<src>`, without shelling out to `keytool`, and configure the
|
|
Maven-compatible repositories declared in `configurations.java.repositories`
|
|
globally for the main JVM build tools.
|
|
|
|
Java ignores the system CA store and only trusts what is in each JRE/JDK's
|
|
own `cacerts` file: JKS format before Java 9, PKCS12 since Java 9. Every
|
|
`cacerts` file found under `usr/` or `opt/` in `<src>` is copied into `<dst>`
|
|
with the configured root certificates added, entirely in Python: `pyjks`
|
|
handles the legacy JKS format, `cryptography` handles PKCS12. The default
|
|
`changeit` store password is used, since virtually every JDK ships with it
|
|
unchanged; a keystore that doesn't open with it is left untouched and a
|
|
warning is logged instead of failing the whole run.
|
|
|
|
The repositories are written to the well-known global configuration file of
|
|
each build tool. Maven's is installation-wide, so every user on the image
|
|
picks it up; the others are written under root's home directory, since this
|
|
tool targets Docker images, which are built and run as root:
|
|
|
|
- Maven3: `/etc/maven/settings.xml` (the file `/usr/share/maven/conf/settings.xml`
|
|
points to on Debian/Ubuntu, Maven's global, all-users settings file), plus
|
|
`conf/settings.xml` in every other Maven installation detected under
|
|
`<src>` (found via its `bin/mvn` launcher). Both get an always-active
|
|
profile adding every repository, plus a `mirrorOf=*` mirror on the first
|
|
one so that even `pom.xml` files hardcoding central are redirected.
|
|
- Gradle: `~/.gradle/init.d/id-configurator-repositories.gradle`, an init
|
|
script adding the repositories to every project and to plugin management.
|
|
- Ivy: `~/.ivy2/ivysettings.xml`, as a chain of `ibiblio` resolvers.
|
|
- SBT: `~/.sbt/repositories`, the resolver override file SBT reads for its
|
|
own bootstrapping and, when combined with the `sbt.override.build.repos`
|
|
system property, for every project's resolvers as well.
|
|
- Leiningen: `~/.lein/profiles.clj`, adding the repositories to the `:user`
|
|
profile.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import logging
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
|
|
import jks
|
|
from cryptography import x509
|
|
from cryptography.hazmat.primitives.serialization import (
|
|
BestAvailableEncryption,
|
|
Encoding,
|
|
pkcs12,
|
|
)
|
|
|
|
from id_configurator.config import JavaRepository, NamedContent
|
|
from id_configurator.context import Context
|
|
from id_configurator.modules.base import Module, register
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_JAVA_SEARCH_ROOTS = ("usr", "opt")
|
|
_STORE_PASSWORD = b"changeit"
|
|
|
|
_MAVEN_GLOBAL_SETTINGS = "etc/maven/settings.xml"
|
|
_MAVEN_BIN_GLOBS = ("usr/**/bin/mvn", "opt/**/bin/mvn")
|
|
_GRADLE_INIT_SCRIPT = "root/.gradle/init.d/id-configurator-repositories.gradle"
|
|
_IVY_SETTINGS = "root/.ivy2/ivysettings.xml"
|
|
_SBT_REPOSITORIES = "root/.sbt/repositories"
|
|
_LEIN_PROFILES = "root/.lein/profiles.clj"
|
|
|
|
|
|
def _find_cacerts(context: Context) -> list[Path]:
|
|
found = {
|
|
path
|
|
for root in _JAVA_SEARCH_ROOTS
|
|
for path in context.glob_src(f"{root}/**/cacerts")
|
|
}
|
|
return sorted(path for path in found if path.is_file())
|
|
|
|
|
|
def _decode_certificates(certificates: list[NamedContent]) -> list[x509.Certificate]:
|
|
return [
|
|
x509.load_pem_x509_certificate(base64.b64decode(cert.content))
|
|
for cert in certificates
|
|
]
|
|
|
|
|
|
def _add_to_pkcs12(
|
|
content: bytes, certificates: list[NamedContent], certs: list[x509.Certificate]
|
|
) -> bytes:
|
|
loaded = pkcs12.load_pkcs12(content, _STORE_PASSWORD)
|
|
entries = list(loaded.additional_certs)
|
|
for named, cert in zip(certificates, certs, strict=True):
|
|
entries.append(pkcs12.PKCS12Certificate(cert, named.name.encode("utf-8")))
|
|
return pkcs12.serialize_java_truststore(
|
|
entries, BestAvailableEncryption(_STORE_PASSWORD)
|
|
)
|
|
|
|
|
|
def _add_to_jks(
|
|
content: bytes, certificates: list[NamedContent], certs: list[x509.Certificate]
|
|
) -> bytes:
|
|
store = jks.KeyStore.loads(content, _STORE_PASSWORD.decode("ascii"))
|
|
for named, cert in zip(certificates, certs, strict=True):
|
|
store.entries[named.name] = jks.TrustedCertEntry.new(
|
|
named.name, cert.public_bytes(Encoding.DER)
|
|
)
|
|
return store.saves(_STORE_PASSWORD.decode("ascii"))
|
|
|
|
|
|
def _keystore_kind(content: bytes) -> str:
|
|
if content[:4] in (jks.MAGIC_NUMBER_JKS, jks.MAGIC_NUMBER_JCEKS):
|
|
return "jks"
|
|
return "pkcs12"
|
|
|
|
|
|
def _string_literal(value: str) -> str:
|
|
return json.dumps(value)
|
|
|
|
|
|
def _render_xml(root: ET.Element) -> bytes:
|
|
ET.indent(root)
|
|
content = '<?xml version="1.0" encoding="UTF-8"?>\n' + ET.tostring(
|
|
root, encoding="unicode"
|
|
)
|
|
return (content + "\n").encode("utf-8")
|
|
|
|
|
|
def _write_xml(context: Context, relative: str, root: ET.Element) -> None:
|
|
context.write_file(relative, _render_xml(root))
|
|
|
|
|
|
def _find_maven_homes(context: Context) -> list[Path]:
|
|
found = {
|
|
path.parent.parent
|
|
for pattern in _MAVEN_BIN_GLOBS
|
|
for path in context.glob_src(pattern)
|
|
if path.is_file()
|
|
}
|
|
return sorted(found)
|
|
|
|
|
|
def _build_maven_settings(repositories: list[JavaRepository]) -> ET.Element:
|
|
settings = ET.Element(
|
|
"settings",
|
|
{
|
|
"xmlns": "http://maven.apache.org/SETTINGS/1.1.0",
|
|
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
"xsi:schemaLocation": (
|
|
"http://maven.apache.org/SETTINGS/1.1.0 "
|
|
"http://maven.apache.org/xsd/settings-1.1.0.xsd"
|
|
),
|
|
},
|
|
)
|
|
profile = ET.SubElement(ET.SubElement(settings, "profiles"), "profile")
|
|
ET.SubElement(profile, "id").text = "id-configurator"
|
|
activation = ET.SubElement(profile, "activation")
|
|
ET.SubElement(activation, "activeByDefault").text = "true"
|
|
repositories_el = ET.SubElement(profile, "repositories")
|
|
plugin_repositories_el = ET.SubElement(profile, "pluginRepositories")
|
|
for repository in repositories:
|
|
for parent, tag in (
|
|
(repositories_el, "repository"),
|
|
(plugin_repositories_el, "pluginRepository"),
|
|
):
|
|
entry = ET.SubElement(parent, tag)
|
|
ET.SubElement(entry, "id").text = repository.name
|
|
ET.SubElement(entry, "url").text = repository.url
|
|
active_profiles = ET.SubElement(settings, "activeProfiles")
|
|
ET.SubElement(active_profiles, "activeProfile").text = "id-configurator"
|
|
mirror = ET.SubElement(ET.SubElement(settings, "mirrors"), "mirror")
|
|
ET.SubElement(mirror, "id").text = repositories[0].name
|
|
ET.SubElement(mirror, "url").text = repositories[0].url
|
|
ET.SubElement(mirror, "mirrorOf").text = "*"
|
|
return settings
|
|
|
|
|
|
def _write_maven_settings(context: Context, repositories: list[JavaRepository]) -> None:
|
|
content = _render_xml(_build_maven_settings(repositories))
|
|
relatives = {_MAVEN_GLOBAL_SETTINGS}
|
|
relatives.update(
|
|
f"{home.relative_to(context.src).as_posix()}/conf/settings.xml"
|
|
for home in _find_maven_homes(context)
|
|
)
|
|
for relative in sorted(relatives):
|
|
context.write_file(relative, content)
|
|
logger.info(
|
|
"configured maven mirrors for all users in %d location(s)", len(relatives)
|
|
)
|
|
|
|
|
|
def _gradle_repository_lines(repositories: list[JavaRepository], indent: str) -> str:
|
|
return "\n".join(
|
|
f"{indent}maven {{ url = uri({_string_literal(repository.url)}) }}"
|
|
for repository in repositories
|
|
)
|
|
|
|
|
|
def _write_gradle_init_script(
|
|
context: Context, repositories: list[JavaRepository]
|
|
) -> None:
|
|
content = (
|
|
"allprojects {\n"
|
|
" repositories {\n"
|
|
f"{_gradle_repository_lines(repositories, ' ')}\n"
|
|
" }\n"
|
|
"}\n"
|
|
"\n"
|
|
"settingsEvaluated { settings ->\n"
|
|
" settings.pluginManagement {\n"
|
|
" repositories {\n"
|
|
f"{_gradle_repository_lines(repositories, ' ')}\n"
|
|
" }\n"
|
|
" }\n"
|
|
"}\n"
|
|
)
|
|
context.write_file(_GRADLE_INIT_SCRIPT, content.encode("utf-8"))
|
|
logger.info("configured gradle to use %d repositories", len(repositories))
|
|
|
|
|
|
def _write_ivy_settings(context: Context, repositories: list[JavaRepository]) -> None:
|
|
ivysettings = ET.Element("ivysettings")
|
|
ET.SubElement(ivysettings, "settings", {"defaultResolver": "id-configurator"})
|
|
chain = ET.SubElement(
|
|
ET.SubElement(ivysettings, "resolvers"), "chain", {"name": "id-configurator"}
|
|
)
|
|
for repository in repositories:
|
|
ET.SubElement(
|
|
chain,
|
|
"ibiblio",
|
|
{"name": repository.name, "m2compatible": "true", "root": repository.url},
|
|
)
|
|
_write_xml(context, _IVY_SETTINGS, ivysettings)
|
|
logger.info("configured ivy to use %d repositories", len(repositories))
|
|
|
|
|
|
def _write_sbt_repositories(
|
|
context: Context, repositories: list[JavaRepository]
|
|
) -> None:
|
|
lines = ["[repositories]", " local"]
|
|
lines.extend(
|
|
f" {repository.name}: {repository.url}" for repository in repositories
|
|
)
|
|
context.write_file(_SBT_REPOSITORIES, ("\n".join(lines) + "\n").encode("utf-8"))
|
|
logger.info("configured sbt to use %d repositories", len(repositories))
|
|
|
|
|
|
def _write_lein_profiles(context: Context, repositories: list[JavaRepository]) -> None:
|
|
entries = " ".join(
|
|
f"[{_string_literal(repository.name)} {{:url {_string_literal(repository.url)}}}]"
|
|
for repository in repositories
|
|
)
|
|
content = f"{{:user {{:repositories [{entries}]}}}}\n"
|
|
context.write_file(_LEIN_PROFILES, content.encode("utf-8"))
|
|
logger.info("configured leiningen to use %d repositories", len(repositories))
|
|
|
|
|
|
def _configure_repositories(
|
|
context: Context, repositories: list[JavaRepository]
|
|
) -> None:
|
|
_write_maven_settings(context, repositories)
|
|
_write_gradle_init_script(context, repositories)
|
|
_write_ivy_settings(context, repositories)
|
|
_write_sbt_repositories(context, repositories)
|
|
_write_lein_profiles(context, repositories)
|
|
|
|
|
|
@register
|
|
class JavaModule(Module):
|
|
name = "java"
|
|
|
|
def is_relevant(self, context: Context) -> bool:
|
|
has_certificates = bool(context.config.configurations.certificates) and bool(
|
|
_find_cacerts(context)
|
|
)
|
|
has_repositories = bool(context.config.configurations.java.repositories)
|
|
return has_certificates or has_repositories
|
|
|
|
def apply(self, context: Context) -> None:
|
|
certificates = context.config.configurations.certificates
|
|
if certificates:
|
|
certs = _decode_certificates(certificates)
|
|
for path in _find_cacerts(context):
|
|
relative = path.relative_to(context.src).as_posix()
|
|
content = path.read_bytes()
|
|
kind = _keystore_kind(content)
|
|
try:
|
|
if kind == "jks":
|
|
updated = _add_to_jks(content, certificates, certs)
|
|
else:
|
|
updated = _add_to_pkcs12(content, certificates, certs)
|
|
context.write_file(relative, updated)
|
|
logger.info(
|
|
"added %d certificate(s) to java keystore %s",
|
|
len(certificates),
|
|
relative,
|
|
)
|
|
except (jks.KeystoreException, ValueError) as exc:
|
|
logger.warning("skipping java keystore %s: %s", relative, exc)
|
|
|
|
repositories = context.config.configurations.java.repositories
|
|
if repositories:
|
|
_configure_repositories(context, repositories)
|