Skip to content

About

A Django platform for sharing FABRIC research artifacts. Provides a REST API (DRF + OpenAPI docs) and Bootstrap 5 web UI with visibility-based access control (author/project/public). Supports artifact versioning, file uploads, search by title/tag/project, and integrates with FABRIC's CILogon auth and Core API for user/project resolution.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

FABRIC Artifact Manager

A platform for sharing and reproducing FABRIC research artifacts. Built with Django 6.0, Django REST Framework, and drf-spectacular (OpenAPI docs). Provides both a REST API and a Bootstrap 5 web UI.

DISCLAIMER: The code herein may not be up to date nor compliant with the most recent package and/or security notices. The frequency at which this code is reviewed and updated is based solely on the lifecycle of the project for which it was written to support, and is not actively maintained outside of that scope. Use at your own risk.

Table of Contents

Configuration

cp env.template .env
# Edit .env with appropriate values
source .env
uv sync          # creates .venv and installs dependencies

For Docker deployments, also configure:

cp vouch/config.template vouch/config
# Choose a compose template:
#   compose/docker-compose.yml.prod-ssl   — production with Nginx SSL
#   compose/docker-compose.yml.local-ssl  — local development with Nginx SSL
cp compose/docker-compose.yml.prod-ssl docker-compose.yml

See env.template for the full list of environment variables.

Deployment-specific settings

Everything that varies between deployments is read from .env, so the tracked files stay generic and a checkout can be brought forward with git pull without losing local changes. Do not hand-edit docker-compose.yml, nginx/default.conf.template or settings.py for a single environment — set these instead:

Variable Purpose
DJANGO_ALLOWED_HOSTS Comma-separated hosts this deployment answers on. Loopback is always allowed.
DJANGO_CORS_ALLOWED_ORIGINS Comma-separated origins permitted to call the API cross-site.
DJANGO_ADMIN_ENABLED Expose the Django admin at /admin to FABRIC users holding ARTIFACT_MANAGER_ADMINS_ROLE; default false (404 for everyone).
HOST_ARTIFACT_STORAGE Host directory holding artifact bundles.
HOST_ARTIFACT_BACKUPS Host directory holding database backups.
HOST_DB_DATA Host directory backing the PostgreSQL data volume.
HOST_METRICS_LOGS Host directory holding the metrics log — see Logging.
NGINX_HTTP_PORT / NGINX_HTTPS_PORT Host ports published by the Nginx container.
NGINX_SSL_CERTS_DIR Host directory mounted read-only at /etc/ssl.
AMGR_SSL_CERT / AMGR_SSL_CERT_KEY Certificate filenames within that directory.
AMGR_HTTPS_PORT Public HTTPS port used in redirects.
AMGR_SERVER_NAMES Required. Space-separated names Nginx serves (as DJANGO_ALLOWED_HOSTS, IPv6 in brackets); any other or missing Host is closed with 444.
AMGR_CLIENT_MAX_BODY_SIZE Largest artifact bundle accepted on upload.
USE_X_ACCEL_REDIRECT Delegate artifact downloads to Nginx — see Artifact downloads.
GITHUB_OIDC_AUDIENCE Public site URL expected in GitHub Actions tokens; empty disables Actions publishing.
GITHUB_IMPORT_MAX_BYTES Maximum compressed source archive size; default 78643200 (75 MiB).
GITHUB_IMPORTS_PER_DAY New GitHub versions allowed per artifact per UTC day; default 10.
GITHUB_API_TOKEN Optional server-side GitHub API token; recommended to increase the rate limit.
CRAWLER_USER_AGENT_MARKERS Comma-separated substrings matched case-insensitively against User-Agent; matching requests are served but not counted as views or downloads. curl, wget and python-requests count as real.

nginx/default.conf.template is rendered at container start by the official Nginx image's envsubst entrypoint. Only ${AMGR_*} placeholders are substituted, so Nginx's own runtime variables ($host, $request_uri, ...) pass through untouched.

Django serves /robots.txt itself in every run mode, so no Nginx change is needed. It keeps well-behaved crawlers off the API, the Django admin, the authentication paths and the create and update forms while leaving the artifact listing and detail pages indexable. Crawlers that ignore it are still served, but requests whose User-Agent matches CRAWLER_USER_AGENT_MARKERS do not add to an artifact's view or download counts.

Deploy

Local development server

source .env
./run_server.sh --run-mode local-dev --make-migrations --load-fixtures

Local with uWSGI + Nginx + SSL

source .env
UWSGI_UID=$(id -u) UWSGI_GID=$(id -g) ./run_server.sh --run-mode local-ssl --make-migrations --load-fixtures

Docker

MAKE_MIGRATIONS=1 LOAD_FIXTURES=1 docker compose up -d

Artifact downloads

Bundles reach several hundred megabytes, so a download is never buffered in memory. Two strategies, selected by USE_X_ACCEL_REDIRECT:

  • false (default) — Django streams the file with FileResponse. Correct in every run mode, and the only option for --run-mode local-dev. A uWSGI worker stays occupied for the length of the transfer.
  • true — Django authorizes the request, records the download and returns headers only, naming an internal Nginx location through the X-Accel-Redirect header. Nginx sends the bytes with sendfile(), so the worker is released immediately and byte-range and conditional requests are handled by Nginx. Requires the bundled Nginx with HOST_ARTIFACT_STORAGE mounted, which the supplied compose files do.

Permission checks are identical either way: the Nginx location is marked internal, so it cannot be requested directly and every download still passes through validate_contents_download().

Logging

Two loggers, configured by the LOGGING dictConfig in artifactmgr/server/settings.py:

  • consoleLogger — operational output to stdout, where docker compose logs django picks it up. Django's own records share this stream, so django.request 4xx warnings and 5xx tracebacks are visible in production rather than swallowed.
  • metricsLogger — the audit event stream. It writes to its own file and never propagates into stdout, so the event record stays clean enough to grep and parse.

All timestamps are UTC, in both streams, regardless of the host timezone.

Levels are set per logger in .env, each variable gating exactly one logger:

Variable Purpose
DJANGO_LOG_LEVEL Django's own loggers. Ships as INFO; DEBUG is very chatty.
ROOT_LOG_LEVEL Everything not covered by a more specific logger, third-party libraries included.
DJANGO_DB_LOG_LEVEL Pinned separately so DJANGO_LOG_LEVEL=DEBUG cannot also switch on per-query SQL logging.
CONSOLE_LOG_LEVEL First-party operational output — consoleLogger and the artifactmgr package logger.
METRICS_LOG_LEVEL The metrics event stream.
METRICS_LOG_FILE Path to the metrics log. A relative path resolves against the project root, /code under Docker.
METRICS_LOG_MAX_BYTES 0 (default) uses WatchedFileHandler with external rotation. See the warning below.
METRICS_LOG_BACKUP_COUNT Backup generations kept when METRICS_LOG_MAX_BYTES > 0.

The metrics log directory must exist before start

METRICS_LOG_FILE defaults to ./logs/metrics/metrics.log, and under Docker the containing directory is the bind mount named by HOST_METRICS_LOGS (default ./logs/metrics, which works out of the box for local development; point it at durable storage in production).

The handler opens the file at process startup rather than at first write. That is deliberate — a bad path fails loudly at boot instead of silently discarding every audit record — but it means the directory must already exist and be writable by the uWSGI worker uid, or Django will not start. Under Docker, docker-entrypoint.sh creates it and chowns it to UWSGI_UID/UWSGI_GID while still running as root; the chown is a warning rather than a fatal error, because it is a no-op on Docker Desktop's virtiofs and on NFS mounts. For a deployment whose HOST_METRICS_LOGS lives outside the checkout, pre-create the host directory and chown it to the same uid before the first docker compose up.

Rotation

Rotation is external. Install the supplied logrotate fragment, editing the path and the create uid/gid to match this deployment's .env:

sudo cp logrotate/artifact-manager-metrics /etc/logrotate.d/artifact-manager-metrics
sudo logrotate -d /etc/logrotate.d/artifact-manager-metrics   # dry run, verify the paths

No signal or restart is needed afterwards: WatchedFileHandler notices the inode change and reopens the file on its own. The fragment deliberately does not use copytruncate, which would keep the inode and lose every record written between the copy and the truncate.

Setting METRICS_LOG_MAX_BYTES above 0 switches to an in-process RotatingFileHandler instead. That is local development only, and only for a single process: artifactmgr.ini runs four uWSGI workers with no lazy-apps, so all four inherit one file description, all four see the same offset, and they stampede the rollover — destroying backup generations and losing exactly the records an audit is later asked to produce.

Container stdout is capped by the compose logging: block on the django service (10 MB per file, 5 files) so that raising DJANGO_LOG_LEVEL to DEBUG cannot fill the host disk.

Web UI

The web UI provides the following pages:

Page URL Description
Artifacts /artifacts/ Paginated list of all artifacts with search by title, tag, or project name
Artifacts by Author /artifacts/authors/ Authors table with per-author artifact counts
Author Detail /artifacts/authors/<uuid> Paginated artifacts by a specific author with search by title, tag, or project name
Artifacts by Project /artifacts/projects/ Projects table with per-project artifact counts and search
Project Detail /artifacts/projects/<uuid> Paginated artifacts for a specific FABRIC project with search by title, tag, or project name
Artifact Detail /artifacts/<uuid> Artifact metadata, versions, and file management
Create Artifact /artifacts/create/ Form to create a new artifact (authenticated users)
Update Artifact /artifacts/<uuid>/update Form to edit an artifact (authors only)

All list views enforce visibility-based authorization:

  • Public artifacts are visible to everyone
  • Project artifacts are visible to project members and authors
  • Author artifacts are visible only to their authors

REST API

Interactive API documentation is available at:

  • /api/swagger/ — Swagger UI
  • /api/redoc/ — ReDoc UI
  • /api/schema/ — OpenAPI 3 schema (JSON)

Artifact endpoints

Endpoint Query Parameters Description
GET /api/artifacts search, page List all visible artifacts with search by title, tag, or project name
POST /api/artifacts Create a new artifact
GET /api/artifacts/{uuid} Retrieve a specific artifact
PUT /api/artifacts/{uuid} Update a specific artifact
PATCH /api/artifacts/{uuid} Partially update a specific artifact
DELETE /api/artifacts/{uuid} Delete a specific artifact (permanent; removes all versions and stored files)
GET /api/artifacts/{uuid}/repo Show the linked repository, subject to artifact visibility and author redaction
PUT /api/artifacts/{uuid}/repo Author: link or re-confirm with JSON {"repository": "owner/repo"}
DELETE /api/artifacts/{uuid}/repo Author: unlink; existing versions are retained
GET /api/artifacts/{uuid}/repo/releases Author: list published releases from up to 500 GitHub releases, marked when imported
POST /api/artifacts/{uuid}/repo/import Author: import with JSON {"tag": "v1.0"} and FABRIC authentication
POST /api/artifacts/{uuid}/repo/publish GitHub Actions: import with JSON {"tag": "v1.0"} and an OIDC bearer token
GET /api/artifacts/by-author/{uuid} search, page List artifacts by a specific author with search by title, tag, or project name
GET /api/artifacts/by-project/{uuid} search, page List artifacts for a specific project with search by title, tag, or project name

Other endpoints

Endpoint Description
GET /api/authors List all artifact authors
GET /api/authors/{uuid} Retrieve a specific author
GET /api/contents List artifact versions/content
GET /api/contents/{uuid} Retrieve a specific version
GET /api/contents/download/{urn} Download an artifact version by URN
GET /api/meta/tags List all artifact tags

List endpoints enforce visibility-based authorization. The GitHub release list returns one bounded list; other list endpoints support pagination. Repository write actions accept JSON, with Content-Type: application/json; the existing contents upload endpoint still accepts only uploaded fabric bundles.

Version responses include storage_type and source. For a GitHub version, version is its tag and source records the repository, tag, full commit, release URL/name/date, trigger and pre-release flag; uploaded versions have source: null. When authors are hidden, non-authors receive repository: null, source: null, and a date-based version label in both artifact and /api/contents responses.

Publishing releases from GitHub

The site's own usage guide at /help/ covers this for artifact authors, including the GitHub Actions setup; this section is the operator-side summary.

An artifact author can link a public GitHub repository from the artifact detail page. Enter owner/repo or https://github.com/owner/repo, then choose Link repository. Anyone with write access to that repository can then publish versions of this artifact through GitHub Actions. Choose Show releases and tags and Import to copy a published release, or a git tag that has no release, manually; the equivalent API call is POST /api/artifacts/{uuid}/repo/import with {"tag": "v1.0"} and the author's FABRIC token. A tag without a published release is imported with its commit, a tree/<tag> link and the commit date, but no release name or pre-release flag.

The source tarball is copied into FABRIC storage at the resolved commit. GitHub and uploaded versions can coexist, and downloads follow the artifact's visibility even after the repository is unlinked. Each GitHub version shows its tag, commit, release link and pre-release badge. Hiding authors also hides the repository and GitHub provenance from non-authors.

Automatic publishing

On the artifact page, expand Publish automatically with GitHub Actions and copy the workflow into .github/workflows/publish-artifact.yml in the linked repository. The page fills in the site URL and artifact UUID; for the example below, replace both values. The site administrator must set GITHUB_OIDC_AUDIENCE to that exact public site URL to enable Actions publishing.

name: Publish to FABRIC Artifact Manager
# Runs for a published release and for a pushed tag. When one action does both, the
# second run finds the version already imported and succeeds without a second copy.
on:
  release:
    types: [published]
  push:
    tags: ['*']
permissions:
  id-token: write
  contents: read
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Publish ${{ github.event.release.tag_name || github.ref_name }}
        env:
          AMGR_URL: "https://artifacts.example.org"
          ARTIFACT_UUID: "replace-with-artifact-uuid"
          TAG: ${{ github.event.release.tag_name || github.ref_name }}
        run: |
          set -euo pipefail
          OIDC_TOKEN=$(curl -sSf --get --data-urlencode "audience=$AMGR_URL" \
            -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r .value)
          curl -sS --fail-with-body --retry 5 --retry-delay 15 --retry-all-errors -X POST \
            "${AMGR_URL%/}/api/artifacts/$ARTIFACT_UUID/repo/publish" \
            -H "Authorization: Bearer $OIDC_TOKEN" -H 'Content-Type: application/json' \
            --data "$(jq -cn --arg tag "$TAG" '{tag: $tag}')"

GitHub supplies a short-lived OIDC token for each run, so no stored Artifact Manager secret is needed. The audience request is URL-encoded; a trailing slash in the configured audience is preserved for token verification and removed only when constructing the publish endpoint URL.

Trust model

The publish endpoint verifies an RS256 signature from https://token.actions.githubusercontent.com, token expiry and the exact configured audience, and requires the linked public repository and owner IDs to match the token. Imports are attributed to the author who confirmed the link and require that person to remain an author; pull_request and pull_request_target events are refused, while other events (including release and workflow_dispatch) are allowed.

If the confirming author is removed, the repository card shows Publishing paused until a current author chooses Re-confirm. A repository transfer also needs author re-confirmation. Unlink stops future publishing and preserves previously imported versions.

Limits

  • Only public repositories on GitHub.com are supported. GitHub Enterprise OIDC issuers are unsupported. Published releases (including pre-releases) and git tags without a published release can be imported; a draft release is treated as a bare tag. Attached release assets are not imported.
  • Source archives are limited to 75 MiB (78643200 bytes) by default, configurable through GITHUB_IMPORT_MAX_BYTES. Downloads have a 50-second budget shared with metadata requests; stalled reads time out after at most 15 seconds.
  • The default cap is 10 new imports per artifact per UTC day (GITHUB_IMPORTS_PER_DAY), shared by web, API and Actions publishing. Re-importing the same repository ID and tag returns the existing version (HTTP 200; a new version returns 201), even when the cap has been reached.
  • Listing reads at most five pages of 100 GitHub releases and five pages of 100 tags. Older tags can still be imported by name through the API. Two imports can run concurrently across the site; busy, rate-limited or timed-out requests can be retried. GITHUB_API_TOKEN is recommended for production to raise GitHub's API rate limit.
  • There is no jti replay tracking. A captured token remains valid for its few minutes, including the verification leeway, and can only import already-published tags of the linked repository.

Backup and Restore

The dumpdata.sh script creates a timestamped backup under ./backups/ containing:

  • Django JSON fixtures for each app (apiuser, artifacts) with --natural-foreign --natural-primary for portability
  • A copy of the artifact storage directory (uploaded .tgz files)
  • A copy of the media directory (if present)
  • A manifest.json with timestamp, app list, and Django version

A ./backups/latest symlink always points to the most recent backup. Fixtures are also copied to ./dumpdata/ for use with --load-fixtures.

Backup

Local:

source .env
uv run ./dumpdata.sh

Preview commands without writing anything:

source .env
uv run ./dumpdata.sh --dry-run

Docker:

docker exec amgr-django /bin/bash -c "source .env; uv run ./dumpdata.sh"
docker cp amgr-django:/code/dumpdata/. dumpdata/

Restore

Local:

cp dumpdata/apiuser.json artifactmgr/apps/apiuser/fixtures/
cp dumpdata/artifacts.json artifactmgr/apps/artifacts/fixtures/
# first run, make migrations, load fixtures
UWSGI_UID=$(id -u) UWSGI_GID=$(id -g) ./run_server.sh --run-mode local-ssl --load-fixtures --make-migrations
# subsequent runs, no need to load fixtures
UWSGI_UID=$(id -u) UWSGI_GID=$(id -g) ./run_server.sh --run-mode local-ssl --make-migrations

Docker:

cp dumpdata/apiuser.json artifactmgr/apps/apiuser/fixtures/
cp dumpdata/artifacts.json artifactmgr/apps/artifacts/fixtures/
docker compose build
MAKE_MIGRATIONS=1 LOAD_FIXTURES=1 docker compose up -d

References

About

A Django platform for sharing FABRIC research artifacts. Provides a REST API (DRF + OpenAPI docs) and Bootstrap 5 web UI with visibility-based access control (author/project/public). Supports artifact versioning, file uploads, search by title/tag/project, and integrates with FABRIC's CILogon auth and Core API for user/project resolution.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages