Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#69 add apt proxy repo support #70

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ Check and, if you would like, change the following environment variables for the
| 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_REMOTE_URL | URL of the APT Remote repository (`http://deb.debian.org/debian` by default) |
| APT_DISTRO | Name of the APT distribution (`bookworm` by default) |
| APT_ALLOWED_ARCHIVES | Comma-separated list of the authorized APT archives (`main,contrib,non-free-firmware,non-free` by default) |

Example allowlist files are included in the repository for [PyPI](allowlists/pypi.allowlist) and [CRAN](allowlists/cran.allowlist).
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.
Expand Down Expand Up @@ -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)):
Expand All @@ -119,4 +138,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
13 changes: 13 additions & 0 deletions allowlists/apt.allowlist
Original file line number Diff line number Diff line change
@@ -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
11 changes: 6 additions & 5 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ 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

timestamp() {
date -Is
}

hashes() {
md5sum $PYPI_ALLOWLIST $CRAN_ALLOWLIST
md5sum $PYPI_ALLOWLIST $CRAN_ALLOWLIST $APT_ALLOWLIST
}

# Ensure allowlist files exist
Expand All @@ -37,7 +38,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"
else
echo "$(timestamp) No initial password file found, skipping initial configuration"
fi
Expand All @@ -51,19 +52,19 @@ 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"
# 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"
hash=$new_hash
fi
sleep 5
done
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"
fi
1 change: 1 addition & 0 deletions integration_tests/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions integration_tests/sources.list
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
deb http://localhost:8080/repository/apt-proxy bookworm main
2 changes: 1 addition & 1 deletion nexus_allowlist/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "v0.11.0"
__version__ = "v0.12.0"
116 changes: 103 additions & 13 deletions nexus_allowlist/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
from pathlib import Path

from nexus_allowlist.nexus import NexusAPI, RepositoryType
from nexus_allowlist.settings import (
ALLOWED_ARCHIVES,
APT_DISTRO,
APT_REMOTE_URL,
)


@dataclass
Expand All @@ -17,12 +22,17 @@ class Repository:
"pypi_proxy": Repository(
repo_type=RepositoryType.PYPI,
name="pypi-proxy",
remote_url="https://pypi.org/",
remote_url="https://pypi.org",
),
"cran_proxy": Repository(
repo_type=RepositoryType.CRAN,
name="cran-proxy",
remote_url="https://cran.r-project.org/",
remote_url="https://cran.r-project.org",
),
"apt_proxy": Repository(
repo_type=RepositoryType.APT,
name="apt-proxy",
remote_url=APT_REMOTE_URL,
),
}

Expand All @@ -37,36 +47,44 @@ 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)

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]:
Expand All @@ -83,9 +101,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
Expand All @@ -94,6 +112,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()
Expand All @@ -104,7 +124,7 @@ def get_allowlist(allowlist_path: Path, repo_type: RepositoryType) -> list[str]:

def recreate_repositories(nexus_api: NexusAPI) -> 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
Expand All @@ -125,6 +145,7 @@ def recreate_privileges(
nexus_api: NexusAPI,
pypi_allowlist: list[str],
cran_allowlist: list[str],
apt_allowlist: list[str],
) -> list[str]:
"""
Create content selectors and content selector privileges based on the
Expand All @@ -134,6 +155,7 @@ def recreate_privileges(
nexus_api: NexusAPI object
pypi_allowlist: List of allowed PyPI packages
cran_allowlist: List of allowed CRAN packages
apt_allowlist: List of allowed APT packages

Returns:
List of the names of all content selector privileges
Expand All @@ -148,6 +170,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
Expand Down Expand Up @@ -185,6 +208,44 @@ def recreate_privileges(
)
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_DISTRO}/.*/Packages.gz"',
JimMadge marked this conversation as resolved.
Show resolved Hide resolved
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_DISTRO}/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_DISTRO}/.*/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":
Expand All @@ -209,6 +270,20 @@ def recreate_privileges(
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=(
'format == "apt" and '
f'path=~"^/pool/({'|'.join(ALLOWED_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:
Expand Down Expand Up @@ -238,7 +313,22 @@ def recreate_privileges(
)
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(ALLOWED_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(
Expand Down
13 changes: 9 additions & 4 deletions nexus_allowlist/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ 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",
)

subparsers = parser.add_subparsers(title="subcommands", required=True)

Expand Down Expand Up @@ -168,7 +173,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
Expand Down Expand Up @@ -234,14 +239,14 @@ 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, pypi_allowlist, cran_allowlist, apt_allowlist
)

# Grant privileges to the nexus allowlist role
Expand Down
Loading
Loading