diff --git a/README.md b/README.md index 3e1c0ff..c1c6e94 100644 --- a/README.md +++ b/README.md @@ -17,16 +17,19 @@ A [Dockerfile](Dockerfile) and example [docker compose](docker-compose.yaml) con Check and, if you would like, change the following environment variables for the Nexus Allowlist container in [`docker-compose.yaml`](./docker-compose.yaml). -| Environment variable | meaning | -| ---------------------- | ------------------------------------------------------------------------------------------------------------- | -| NEXUS_ADMIN_PASSWORD | Password for the Nexus OSS admin user (changes from the default on first rune then used for authentication) | -| NEXUS_PACKAGES | Whether to allow all packages or only selected packages [`all`, `selected`] | -| NEXUS_HOST | Hostname of Nexus OSS host | -| NEXUS_PORT | Port of Nexus OSS | -| NEXUS_PATH | [Context path](https://help.sonatype.com/en/configuring-the-runtime-environment.html#changing-the-context-path) of Nexus OSS. Only used if the Nexus is hosted behind a reverse proxy with a URL like `https://your_url.domain/nexus/`. If not defined, the base URI remains `/`. | -| ENTR_FALLBACK | If defined, don't use `entr` to check for allowlist updates (this will be less reactive but we have found `entr` to not work in some situations) | - -Example allowlist files are included in the repository for [PyPI](allowlists/pypi.allowlist) and [CRAN](allowlists/cran.allowlist). +| Environment variable | meaning | +| ---------------------- | ------------------------------------------------------------------------------------------------------------- | +| NEXUS_ADMIN_PASSWORD | Password for the Nexus OSS admin user (changes from the default on first rune then used for authentication) | +| NEXUS_PACKAGES | Whether to allow all packages or only selected packages [`all`, `selected`] | +| NEXUS_HOST | Hostname of Nexus OSS host | +| NEXUS_PORT | Port of Nexus OSS | +| NEXUS_PATH | [Context path](https://help.sonatype.com/en/configuring-the-runtime-environment.html#changing-the-context-path) of Nexus OSS. Only used if the Nexus is hosted behind a reverse proxy with a URL like `https://your_url.domain/nexus/`. If not defined, the base URI remains `/`. | +| ENTR_FALLBACK | If defined, don't use `entr` to check for allowlist updates (this will be less reactive but we have found `entr` to not work in some situations) | +| APT_URL | URL of the APT Remote repository (`http://deb.debian.org/debian` by default) | +| APT_RELEASE | Name of the APT distribution (`bookworm` by default) | +| APT_ARCHIVES | Allowed APT archives (`main contrib non-free-firmware non-free` by default) | + +Example allowlist files are included in the repository for [PyPI](allowlists/pypi.allowlist), [CRAN](allowlists/cran.allowlist) and [APT](allowlists/apt.allowlist). The PyPI allowlist includes numpy, pandas, matplotlib and their dependencies. The CRAN allowlist includes cli and data.table You can add more packages by writing the package names, one per line, in the allowlist files. @@ -96,6 +99,22 @@ For example, - `install.packages("data.table")` should succeed - `install.packages("ggplot2")` should fail +#### APT + +You can edit '/etc/apt/sources.list' to use the Nexus APT proxy. + +For example + +``` +deb http://localhost:8080/repository/apt-proxy bookworm main +``` + +You should now only be able to install packages from the allowlist. +For example, + +- `sudo apt install libcurl4-openssl-dev` should succeed +- `sudo apt install tcpdump` should fail + ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): @@ -119,4 +138,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/allowlists/apt.allowlist b/allowlists/apt.allowlist new file mode 100644 index 0000000..76a0965 --- /dev/null +++ b/allowlists/apt.allowlist @@ -0,0 +1,13 @@ +r-recommended +r-cran-matrixmodels +libcurl4-openssl-dev +libv8-dev +libxml2-dev +cmake +libfontconfig1-dev +libharfbuzz-dev +libfribidi-dev +libfreetype6-dev +libpng-dev +libtiff5-dev +libjpeg-dev \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 3cea716..f8f07ca 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,13 +4,26 @@ export NEXUS_DATA_DIR=/nexus-data export ALLOWLIST_DIR=/allowlists export PYPI_ALLOWLIST="$ALLOWLIST_DIR"/pypi.allowlist export CRAN_ALLOWLIST="$ALLOWLIST_DIR"/cran.allowlist +export APT_ALLOWLIST="$ALLOWLIST_DIR"/apt.allowlist + +if [ -z "$APT_URL" ]; then + export APT_URL="https://deb.debian.org/debian" +fi + +if [ -z "$APT_RELEASE" ]; then + export APT_RELEASE="bookworm" +fi + +if [ -z "$APT_ARCHIVES" ]; then + export APT_ARCHIVES="main contrib non-free-firmware non-free" +fi timestamp() { date -Is } hashes() { - md5sum $PYPI_ALLOWLIST $CRAN_ALLOWLIST + md5sum $PYPI_ALLOWLIST $CRAN_ALLOWLIST $APT_ALLOWLIST } # Ensure allowlist files exist @@ -37,7 +50,7 @@ nexus-allowlist --version if [ -f "$NEXUS_DATA_DIR/admin.password" ]; then echo "$(timestamp) Initial password file present, running initial configuration" nexus-allowlist --admin-password "$NEXUS_ADMIN_PASSWORD" --nexus-host "$NEXUS_HOST" --nexus-path "$NEXUS_PATH" --nexus-port "$NEXUS_PORT" change-initial-password --path "$NEXUS_DATA_DIR" - nexus-allowlist --admin-password "$NEXUS_ADMIN_PASSWORD" --nexus-host "$NEXUS_HOST" --nexus-path "$NEXUS_PATH" --nexus-port "$NEXUS_PORT" initial-configuration --packages "$NEXUS_PACKAGES" --pypi-package-file "$ALLOWLIST_DIR/pypi.allowlist" --cran-package-file "$ALLOWLIST_DIR/cran.allowlist" + nexus-allowlist --admin-password "$NEXUS_ADMIN_PASSWORD" --nexus-host "$NEXUS_HOST" --nexus-path "$NEXUS_PATH" --nexus-port "$NEXUS_PORT" initial-configuration --packages "$NEXUS_PACKAGES" --pypi-package-file "$PYPI_ALLOWLIST" --cran-package-file "$CRAN_ALLOWLIST" --apt-package-file "$APT_ALLOWLIST" --apt-repository-url "$APT_URL" --apt-repository-release "$APT_RELEASE" --apt-repository-archives "$APT_ARCHIVES" else echo "$(timestamp) No initial password file found, skipping initial configuration" fi @@ -51,13 +64,13 @@ fi if [ -n "$ENTR_FALLBACK" ]; then echo "$(timestamp) Using fallback file monitoring" # Run allowlist configuration now - nexus-allowlist --admin-password "$NEXUS_ADMIN_PASSWORD" --nexus-host "$NEXUS_HOST" --nexus-path "$NEXUS_PATH" --nexus-port "$NEXUS_PORT" update-allowlists --packages "$NEXUS_PACKAGES" --pypi-package-file "$PYPI_ALLOWLIST" --cran-package-file "$CRAN_ALLOWLIST" + nexus-allowlist --admin-password "$NEXUS_ADMIN_PASSWORD" --nexus-host "$NEXUS_HOST" --nexus-path "$NEXUS_PATH" --nexus-port "$NEXUS_PORT" update-allowlists --packages "$NEXUS_PACKAGES" --pypi-package-file "$PYPI_ALLOWLIST" --cran-package-file "$CRAN_ALLOWLIST" --apt-package-file "$APT_ALLOWLIST" --apt-repository-url "$APT_URL" --apt-repository-release "$APT_RELEASE" --apt-repository-archives "$APT_ARCHIVES" # Periodically check for modification of allowlist files and run configuration again when they are hash=$(hashes) while true; do new_hash=$(hashes) if [ "$hash" != "$new_hash" ]; then - nexus-allowlist --admin-password "$NEXUS_ADMIN_PASSWORD" --nexus-host "$NEXUS_HOST" --nexus-path "$NEXUS_PATH" --nexus-port "$NEXUS_PORT" update-allowlists --packages "$NEXUS_PACKAGES" --pypi-package-file "$PYPI_ALLOWLIST" --cran-package-file "$CRAN_ALLOWLIST" + nexus-allowlist --admin-password "$NEXUS_ADMIN_PASSWORD" --nexus-host "$NEXUS_HOST" --nexus-path "$NEXUS_PATH" --nexus-port "$NEXUS_PORT" update-allowlists --packages "$NEXUS_PACKAGES" --pypi-package-file "$PYPI_ALLOWLIST" --cran-package-file "$CRAN_ALLOWLIST" --apt-package-file "$APT_ALLOWLIST" --apt-repository-url "$APT_URL" --apt-repository-release "$APT_RELEASE" --apt-repository-archives "$APT_ARCHIVES" hash=$new_hash fi sleep 5 @@ -65,5 +78,5 @@ if [ -n "$ENTR_FALLBACK" ]; then else echo "$(timestamp) Using entr for file monitoring" # Run allowlist configuration now, and again whenever allowlist files are modified - find "$ALLOWLIST_DIR"/*.allowlist | entr -n nexus-allowlist --admin-password "$NEXUS_ADMIN_PASSWORD" --nexus-host "$NEXUS_HOST" --nexus-path "$NEXUS_PATH" --nexus-port "$NEXUS_PORT" update-allowlists --packages "$NEXUS_PACKAGES" --pypi-package-file "$PYPI_ALLOWLIST" --cran-package-file "$CRAN_ALLOWLIST" + find "$ALLOWLIST_DIR"/*.allowlist | entr -n nexus-allowlist --admin-password "$NEXUS_ADMIN_PASSWORD" --nexus-host "$NEXUS_HOST" --nexus-path "$NEXUS_PATH" --nexus-port "$NEXUS_PORT" update-allowlists --packages "$NEXUS_PACKAGES" --pypi-package-file "$PYPI_ALLOWLIST" --cran-package-file "$CRAN_ALLOWLIST" --apt-package-file "$APT_ALLOWLIST" --apt-repository-url "$APT_URL" --apt-repository-release "$APT_RELEASE" --apt-repository-archives "$APT_ARCHIVES" fi diff --git a/integration_tests/Dockerfile b/integration_tests/Dockerfile index 69190bd..1502f89 100644 --- a/integration_tests/Dockerfile +++ b/integration_tests/Dockerfile @@ -4,3 +4,4 @@ RUN apk add --no-cache --update python3 py3-pip R RUN mkdir -p /root/.config/pip COPY pip.conf /root/.config/pip/pip.conf COPY Rprofile /root/.Rprofile +COPY sources.list /etc/apt/sources.list \ No newline at end of file diff --git a/integration_tests/sources.list b/integration_tests/sources.list new file mode 100644 index 0000000..3a61f46 --- /dev/null +++ b/integration_tests/sources.list @@ -0,0 +1 @@ +deb http://localhost:8080/repository/apt-proxy bookworm main \ No newline at end of file diff --git a/nexus_allowlist/__about__.py b/nexus_allowlist/__about__.py index 2d81ab7..fb6e1af 100644 --- a/nexus_allowlist/__about__.py +++ b/nexus_allowlist/__about__.py @@ -1 +1 @@ -__version__ = "v0.11.0" +__version__ = "v0.12.0" diff --git a/nexus_allowlist/actions.py b/nexus_allowlist/actions.py index 40cd349..0145564 100644 --- a/nexus_allowlist/actions.py +++ b/nexus_allowlist/actions.py @@ -2,6 +2,7 @@ import re from dataclasses import dataclass from pathlib import Path +from typing import Any from nexus_allowlist.nexus import NexusAPI, RepositoryType @@ -13,18 +14,24 @@ class Repository: remote_url: str -_NEXUS_REPOSITORIES = { - "pypi_proxy": Repository( - repo_type=RepositoryType.PYPI, - name="pypi-proxy", - remote_url="https://pypi.org/", - ), - "cran_proxy": Repository( - repo_type=RepositoryType.CRAN, - name="cran-proxy", - remote_url="https://cran.r-project.org/", - ), -} +def get_nexus_repositories(args: argparse.Namespace) -> dict[str, Any]: + return { + "pypi_proxy": Repository( + repo_type=RepositoryType.PYPI, + name="pypi-proxy", + remote_url="https://pypi.org", + ), + "cran_proxy": Repository( + repo_type=RepositoryType.CRAN, + name="cran-proxy", + remote_url="https://cran.r-project.org", + ), + "apt_proxy": Repository( + repo_type=RepositoryType.APT, + name="apt-proxy", + remote_url=args.apt_repository_url, + ), + } def check_package_files(args: argparse.Namespace) -> None: @@ -37,28 +44,33 @@ def check_package_files(args: argparse.Namespace) -> None: raise: Exception: if any declared allowlist file does not exist """ - for package_file in [args.pypi_package_file, args.cran_package_file]: + for package_file in [ + args.pypi_package_file, + args.cran_package_file, + args.apt_package_file, + ]: if package_file and not package_file.is_file(): msg = f"Package allowlist file {package_file} does not exist" raise Exception(msg) def get_allowlists( - pypi_package_file: Path, cran_package_file: Path -) -> tuple[list[str], list[str]]: + pypi_package_file: Path, cran_package_file: Path, apt_package_file: Path +) -> tuple[list[str], list[str], list[str]]: """ - Create allowlists for PyPI and CRAN packages + Create allowlists for PyPI, CRAN and APT packages Args: pypi_package_file: Path to the PyPI allowlist file or None cran_package_file: Path to the CRAN allowlist file or None Returns: - A tuple of the PyPI and CRAN allowlists (in that order). The lists are + A tuple of the PyPI, CRAN and APT allowlists (in that order). The lists are [] if the corresponding package file argument was None """ pypi_allowlist = [] cran_allowlist = [] + apt_allowlist = [] if pypi_package_file: pypi_allowlist = get_allowlist(pypi_package_file, repo_type=RepositoryType.PYPI) @@ -66,7 +78,10 @@ def get_allowlists( if cran_package_file: cran_allowlist = get_allowlist(cran_package_file, repo_type=RepositoryType.CRAN) - return (pypi_allowlist, cran_allowlist) + if apt_package_file: + apt_allowlist = get_allowlist(apt_package_file, repo_type=RepositoryType.APT) + + return (pypi_allowlist, cran_allowlist, apt_allowlist) def get_allowlist(allowlist_path: Path, repo_type: RepositoryType) -> list[str]: @@ -83,9 +98,9 @@ def get_allowlist(allowlist_path: Path, repo_type: RepositoryType) -> list[str]: allowlist = [] with open(allowlist_path) as allowlist_file: # Sanitise package names - # - convert to lower case if the package is on PyPI. Leave alone on CRAN to - # prevent issues with case-sensitivity - for PyPI replace strings of '.', '_' - # or '-' with '-' + # - convert to lower case if the package is on PyPI or APT. Leave alone on CRAN + # to prevent issues with case-sensitivity - for PyPI replace strings of '.', + # '_' or '-' with '-' # https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#name # - remove any blank entries, which act as a wildcard that would allow any # package @@ -94,6 +109,8 @@ def get_allowlist(allowlist_path: Path, repo_type: RepositoryType) -> list[str]: match repo_type: case RepositoryType.CRAN: package_name_parsed = package_name.strip() + case RepositoryType.APT: + package_name_parsed = package_name.lower().strip() case RepositoryType.PYPI: package_name_parsed = pypi_replace_characters.sub( "-", package_name.lower().strip() @@ -102,17 +119,21 @@ def get_allowlist(allowlist_path: Path, repo_type: RepositoryType) -> list[str]: return allowlist -def recreate_repositories(nexus_api: NexusAPI) -> None: +def recreate_repositories( + nexus_api: NexusAPI, + nexus_repositories: dict[str, Any] +) -> None: """ - Create PyPI and CRAN proxy repositories in an idempotent manner + Create PyPI, CRAN and APT proxy repositories in an idempotent manner Args: nexus_api: NexusAPI object + nexus_repositories: A dict of Repository objects """ # Delete all existing repositories nexus_api.delete_all_repositories() - for repository in _NEXUS_REPOSITORIES.values(): + for repository in nexus_repositories.values(): nexus_api.create_proxy_repository( repo_type=repository.repo_type, name=repository.name, @@ -123,8 +144,12 @@ def recreate_repositories(nexus_api: NexusAPI) -> None: def recreate_privileges( packages: str, nexus_api: NexusAPI, + nexus_repositories: dict[str, Any], pypi_allowlist: list[str], cran_allowlist: list[str], + apt_allowlist: list[str], + apt_release: str, + apt_archives: list[str], ) -> list[str]: """ Create content selectors and content selector privileges based on the @@ -132,8 +157,12 @@ def recreate_privileges( Args: nexus_api: NexusAPI object + nexus_repositories: A dict of Repository objects pypi_allowlist: List of allowed PyPI packages cran_allowlist: List of allowed CRAN packages + apt_allowlist: List of allowed APT packages + apt_release: The APT release + apt_archives: List of allowed APT archives Returns: List of the names of all content selector privileges @@ -148,6 +177,7 @@ def recreate_privileges( pypi_privilege_names = [] cran_privilege_names = [] + apt_privilege_names = [] # Content selector and privilege for PyPI 'simple' path, used to search for # packages @@ -156,8 +186,8 @@ def recreate_privileges( name="simple", description="Allow access to 'simple' directory in PyPI repository", expression='format == "pypi" and path=^"/simple"', - repo_type=_NEXUS_REPOSITORIES["pypi_proxy"].repo_type, - repo=_NEXUS_REPOSITORIES["pypi_proxy"].name, + repo_type=nexus_repositories["pypi_proxy"].repo_type, + repo=nexus_repositories["pypi_proxy"].name, ) pypi_privilege_names.append(privilege_name) @@ -168,8 +198,8 @@ def recreate_privileges( name="packages", description="Allow access to 'PACKAGES' file in CRAN repository", expression='format == "r" and path=="/src/contrib/PACKAGES"', - repo_type=_NEXUS_REPOSITORIES["cran_proxy"].repo_type, - repo=_NEXUS_REPOSITORIES["cran_proxy"].name, + repo_type=nexus_repositories["cran_proxy"].repo_type, + repo=nexus_repositories["cran_proxy"].name, ) cran_privilege_names.append(privilege_name) @@ -180,11 +210,49 @@ def recreate_privileges( name="archive", description="Allow access to 'archive.rds' file in CRAN repository", expression='format == "r" and path=="/src/contrib/Meta/archive.rds"', - repo_type=_NEXUS_REPOSITORIES["cran_proxy"].repo_type, - repo=_NEXUS_REPOSITORIES["cran_proxy"].name, + repo_type=nexus_repositories["cran_proxy"].repo_type, + repo=nexus_repositories["cran_proxy"].name, ) cran_privilege_names.append(privilege_name) + # Content selector and privilege for APT 'Packages.gz' file which contains an + # metadata for all archived packages + privilege_name = create_content_selector_and_privilege( + nexus_api, + name="apt-packages", + description="Allow access to 'Packages.gz' file in APT repository", + expression=f'format == "apt" and path=~"^/dists/{apt_release}/.*/Packages.gz"', + repo_type=nexus_repositories["apt_proxy"].repo_type, + repo=nexus_repositories["apt_proxy"].name, + ) + apt_privilege_names.append(privilege_name) + + # Content selector and privilege for APT 'InRelease' file which contains an + # metadata about the APT distribution + privilege_name = create_content_selector_and_privilege( + nexus_api, + name="inrelease", + description="Allow access to 'InRelease' file in APT repository", + expression=f'format == "apt" and path=="/dists/{apt_release}/InRelease"', + repo_type=nexus_repositories["apt_proxy"].repo_type, + repo=nexus_repositories["apt_proxy"].name, + ) + apt_privilege_names.append(privilege_name) + + # Content selector and privilege for APT 'Translation-*' files which contains an + # metadata about the APT distribution + privilege_name = create_content_selector_and_privilege( + nexus_api, + name="apt-translation", + description="Allow access to 'Translation-*' file in APT repository", + expression=( + f'format == "apt" and path=~"^/dists/{apt_release}/.*/Translation-.*"' + ), + repo_type=nexus_repositories["apt_proxy"].repo_type, + repo=nexus_repositories["apt_proxy"].name, + ) + apt_privilege_names.append(privilege_name) + # Create content selectors and privileges for packages according to the # package setting if packages == "all": @@ -194,8 +262,8 @@ def recreate_privileges( name="pypi-all", description="Allow access to all PyPI packages", expression='format == "pypi" and path=^"/packages/"', - repo_type=_NEXUS_REPOSITORIES["pypi_proxy"].repo_type, - repo=_NEXUS_REPOSITORIES["pypi_proxy"].name, + repo_type=nexus_repositories["pypi_proxy"].repo_type, + repo=nexus_repositories["pypi_proxy"].name, ) pypi_privilege_names.append(privilege_name) @@ -205,10 +273,23 @@ def recreate_privileges( name="cran-all", description="Allow access to all CRAN packages", expression='format == "r" and path=^"/src/contrib"', - repo_type=_NEXUS_REPOSITORIES["cran_proxy"].repo_type, - repo=_NEXUS_REPOSITORIES["cran_proxy"].name, + repo_type=nexus_repositories["cran_proxy"].repo_type, + repo=nexus_repositories["cran_proxy"].name, ) cran_privilege_names.append(privilege_name) + + # Allow all APT packages + privilege_name = create_content_selector_and_privilege( + nexus_api, + name="apt-all", + description="Allow access to all APT packages", + expression=( + f'format == "apt" and path=~"^/pool/({'|'.join(apt_archives)})/.*"' + ), + repo_type=nexus_repositories["apt_proxy"].repo_type, + repo=nexus_repositories["apt_proxy"].name, + ) + apt_privilege_names.append(privilege_name) elif packages == "selected": # Allow selected PyPI packages for package in pypi_allowlist: @@ -217,8 +298,8 @@ def recreate_privileges( name=f"pypi-{package}", description=f"Allow access to {package} on PyPI", expression=f'format == "pypi" and path=^"/packages/{package}/"', - repo_type=_NEXUS_REPOSITORIES["pypi_proxy"].repo_type, - repo=_NEXUS_REPOSITORIES["pypi_proxy"].name, + repo_type=nexus_repositories["pypi_proxy"].repo_type, + repo=nexus_repositories["pypi_proxy"].name, ) pypi_privilege_names.append(privilege_name) @@ -233,12 +314,27 @@ def recreate_privileges( f'and (path=^"/src/contrib/{package}_" ' f'or path=^"/src/contrib/Archive/{package}/{package}_")' ), - repo_type=_NEXUS_REPOSITORIES["cran_proxy"].repo_type, - repo=_NEXUS_REPOSITORIES["cran_proxy"].name, + repo_type=nexus_repositories["cran_proxy"].repo_type, + repo=nexus_repositories["cran_proxy"].name, ) cran_privilege_names.append(privilege_name) - return pypi_privilege_names + cran_privilege_names + # Allow selected APT packages + for package in apt_allowlist: + privilege_name = create_content_selector_and_privilege( + nexus_api, + name=f"apt-{package}", + description=f"Allow access to {packages} APT package", + expression=( + 'format == "apt" and ' + f'path=~"^/pool/({'|'.join(apt_archives)})/.*/{package}.*"' + ), + repo_type=nexus_repositories["apt_proxy"].repo_type, + repo=nexus_repositories["apt_proxy"].name, + ) + apt_privilege_names.append(privilege_name) + + return pypi_privilege_names + cran_privilege_names + apt_privilege_names def create_content_selector_and_privilege( diff --git a/nexus_allowlist/cli.py b/nexus_allowlist/cli.py index bad5cfe..a5d0103 100644 --- a/nexus_allowlist/cli.py +++ b/nexus_allowlist/cli.py @@ -76,6 +76,27 @@ def main() -> None: "Path of the file of allowed CRAN packages, ignored when PACKAGES is all" ), ) + packages_parser.add_argument( + "--apt-package-file", + type=Path, + help="Path of the file of allowed APT packages, ignored when PACKAGES is all", + ) + packages_parser.add_argument( + "--apt-repository-url", + type=str, + help="URL of the upstream APT repository to proxy", + ) + packages_parser.add_argument( + "--apt-repository-release", + type=str, + help="The release name for APT packages", + ) + packages_parser.add_argument( + "--apt-repository-archives", + type=str, + nargs="*", + help="APT repository archives to allow", + ) subparsers = parser.add_subparsers(title="subcommands", required=True) @@ -168,7 +189,7 @@ def initial_configuration(args: argparse.Namespace) -> None: This includes: - Deleting all respositories - - Creating CRAN and PyPI proxies + - Creating CRAN, APT and PyPI proxies - Deleting all content selectors and content selector privileges - Deleting all non-default roles - Creating a role @@ -190,7 +211,10 @@ def initial_configuration(args: argparse.Namespace) -> None: ) # Ensure only desired repositories exist - actions.recreate_repositories(nexus_api) + actions.recreate_repositories( + nexus_api, + actions.get_nexus_repositories(args), + ) # Delete non-default roles nexus_api.delete_all_custom_roles() @@ -234,14 +258,21 @@ def update_allow_lists(args: argparse.Namespace) -> None: ) # Parse allowlists - pypi_allowlist, cran_allowlist = actions.get_allowlists( - args.pypi_package_file, args.cran_package_file + pypi_allowlist, cran_allowlist, apt_allowlist = actions.get_allowlists( + args.pypi_package_file, args.cran_package_file, args.apt_package_file ) # Recreate all content selectors and associated privileges according to the # allowlists privileges = actions.recreate_privileges( - args.packages, nexus_api, pypi_allowlist, cran_allowlist + args.packages, + nexus_api, + actions.get_nexus_repositories(args), + pypi_allowlist, + cran_allowlist, + apt_allowlist, + args.apt_release, + args.apt_archives, ) # Grant privileges to the nexus allowlist role diff --git a/nexus_allowlist/nexus.py b/nexus_allowlist/nexus.py index a32fb2f..9d6e5f6 100644 --- a/nexus_allowlist/nexus.py +++ b/nexus_allowlist/nexus.py @@ -21,6 +21,7 @@ class ResponseCode(Enum): class RepositoryType(Enum): PYPI = "pypi" CRAN = "r" + APT = "apt" class NexusAPI: @@ -96,7 +97,7 @@ def create_proxy_repository( self, repo_type: RepositoryType, name: str, remote_url: str ) -> None: """ - Create a proxy repository. Currently supports PyPI and R formats + Create a proxy repository. Currently supports PyPI, R and APT formats Args: repo_type: Type of repository @@ -123,6 +124,8 @@ def create_proxy_repository( } payload["name"] = name payload["proxy"]["remoteUrl"] = remote_url + if repo_type == RepositoryType.APT: + payload["apt"] = {"distribution": APT_DISTRO, "flat": False} logging.info(f"Creating {repo_type.value} repository: {name}") response = requests.post(