Repository and Artifact Management

Context

CodeScoring.Save stores and serves artifacts for a development team: a builder publishes a package, and IDEs, CI agents, and engineers download it from the same URL — without leaving the company perimeter.

All artifacts live inside repositories, and repositories are grouped into projects. The same hierarchy is reflected in the web interface:

Project(e.g. backend-team)
Repository(e.g. maven-central)
Artifact(e.g. commons-lang3:3.12.0)

A project is a container for the team's repositories. A repository is a concrete storage of one format (Maven, npm, Docker, etc.) and one of two types. An artifact is what lives inside a repository and what clients pull.

Two Repository Types

CodeScoring.Save supports only two repository types, and each addresses a different need.

Proxy repository — a caching proxy in front of an external source (for example, Maven Central or npmjs.com). When a client requests an artifact for the first time, Save fetches it from upstream, caches it, and serves it. All subsequent requests are served from the cache — this speeds up builds and reduces dependency on external services. Proxy repositories also benefit from OSA Proxy security policies (when configured), which can block downloads of unsafe components.

Hosted repository — internal storage where the team publishes its own artifacts. This is where internal libraries, vetted third-party components, and build artifacts go.

A single project may contain any number of repositories of either type.

URL Access Scheme

An external client (Maven, npm, Docker, etc.) reaches a repository through URLs of the form:

https://save.example.com/<base>/<format>/<project>/<repository>/<artifact-path>
FormatTemplate
Mavenhttps://save.example.com/cs-save/maven/<project>/<repository>/<group>/<artifact>/<version>/<file>
npmhttps://save.example.com/cs-save/npm/<project>/<repository>/<package>
NuGethttps://save.example.com/cs-save/nuget/<project>/<repository>/v3/index.json
PyPIhttps://save.example.com/cs-save/pypi/<project>/<repository>/simple/
Gohttps://save.example.com/cs-save/go/<project>/<repository> (used as GOPROXY)
Composerhttps://save.example.com/cs-save/php/<project>/<repository>
Rawhttps://save.example.com/cs-save/raw/<project>/<repository>/<filepath>
Debhttps://save.example.com/cs-save/deb/<project>/<repository>
RPMhttps://save.example.com/cs-save/rpm/<project>/<repository>
Alpine (APK)https://save.example.com/cs-save/apk/<project>/<repository>/<branch>/<component>
OCI / Dockerhttps://save.example.com/v2/<project>/<repository>/<image>/...

For OCI / Docker, the standard /v2/ prefix is used as required by the OCI Distribution Spec. Nexus-compatible flat URL routing is also supported — see Working with OCI / Docker.

Web Interface Layout

The left-hand vertical navigation has the following sections:

  • Projects — the main section: a list of projects, with a list of repositories inside each project, and a tree of artifacts inside each repository;
  • Cleanup — automatic cleanup policies for unwanted artifacts;
  • Settings — accounts, roles, service accounts, configuration, the activation key, and the audit log.

At the top of the interface is a global search — opened with the / key, it searches across projects, repositories, and artifacts simultaneously, with real-time suggestions.

Basic Scenario: Connect Your First Repository

Step 1. Create a Project

A project is a container for the repositories that will live inside it. It is convenient to split projects by team, product, or environment.

  1. In the sidebar, choose Projects.
  2. Click Create project in the top-right corner.
  3. Fill in the form:
    • Name — a human-readable project name (for example, Backend Team);
    • Color — a color that helps tell projects apart in lists;
    • Key — a URL-safe project identifier (for example, backend-team). If left empty, it is auto-generated from Name, even when Name does not use Latin characters. Key cannot be changed after creation;
    • Description — an optional project description;
    • Cleanup policy — a two-pane selector listing the existing cleanup policies. It can be left empty and assigned later.
  4. Click Create project.

After creation, the project page opens with an (empty) list of repositories and the project's metadata in the header.

Key vs. Name

Name is shown in the UI and can be changed at any time. Key participates in every URL and API path, so changing it would require reconfiguring every client.

Step 2. Create a Repository in the Project

A repository — concrete storage for artifacts of a single format — is created inside a project.

  1. Open the project you just created.

  2. Click Create repository in the top-right corner of the project page.

  3. Fill in the common fields:

    • Name — a human-readable name (for example, Maven Central (proxy)). If the name mentions a format and a type (for example, npm proxy), the link under the field fills in Format, Type, and Proxy URL (the public upstream) for you;
    • Color — a color for visual distinction;
    • Key — a URL-safe identifier (for example, maven-central). It cannot be changed after creation;
    • Description — an optional description, up to 100 characters.
  4. Choose Format: OCI (Docker / Helm), Maven / Gradle, NPM, NuGet, PyPI, Raw / Generic, Go Modules, Debian / APT, RPM, PHP / Composer, or Alpine / APK. The format and the type cannot be changed after creation.

  5. Choose Type:

    • Proxy — to proxy an external registry;
    • Hosted — to host your own artifacts.
  6. If Proxy is selected, additional fields appear:

    • Proxy URL — the upstream repository URL (for example, https://repo1.maven.org/maven2/);
    • Cache TTL, seconds — the metadata cache lifetime in seconds. For typical external registries, 86400 (one day) is enough.

    Depending on the format, the form also shows format-specific fields — for example, Authentication for the upstream or RPM metadata folder depth.

  7. Optionally attach Cleanup policy — a list of cleanup policies that should run against this repository.

  8. Click Create repository.

After creation, the repository page opens. The header shows the status (Enabled/Disabled), the format, the type, the creation and update timestamps, and — for a proxy repository — a clickable Remote URL link to the upstream.

Changing the repository status

The status (Enabled/Disabled) is set on the edit form: open the repository, click the pencil button (Edit repository) in the header, flip the top Status radio toggle, and click Save. A disabled repository does not accept or serve requests, but it is not deleted and keeps its artifacts.

Step 3. Get Ready-Made Snippets for Connecting Clients

In the right part of the repository header there is a chain-link icon button. Clicking it opens the Useful snippets popover — a set of ready-made configuration fragments and commands tailored to the repository format.

  1. Click the chain-link button in the repository header.
  2. In the card that appears, scroll through the snippets:
    • each snippet has a title (for example, settings.xml, .npmrc, pip.conf, docker login), a syntax-highlighted code block, and a short description;
    • the set of snippets is composed server-side based on the repository's format and type. For Maven, you typically see a <mirror> fragment for settings.xml and a <repository> fragment for pom.xml; for Docker — docker login and docker pull; for npm — a line for .npmrc; and so on.
  3. Click the copy button to the right of the snippet you need — it goes straight to the clipboard.
  4. Paste the snippet into the client's configuration file, or run the command in a terminal.

Step 4. Publish and Download the First Artifact

From this point on, everything happens on the client side — Save accepts the standard requests from each package manager. For a hosted repository, the usual path is publishing via mvn deploy, npm publish, twine upload, or docker push. For a proxy repository, an ordinary pull is enough — Save will cache the artifact on the way through. The web interface has no upload button: artifacts get into a repository only through clients and HTTP requests.

The first request may require authentication — see Connecting Clients and Authentication.

Once a client uploads or downloads something for the first time, the artifact appears in the tree on the repository page.

Working with Artifacts in a Repository

The repository page has two columns: the artifact tree with search and sort on the left, the details of the selected artifact on the right.

The Security report tab in the artifact details shows data from the CodeScoring database: package information (PURL, licenses, maintainers, release date) and the vulnerabilities found in the package, grouped by source (NVD, BDU FSTEC, GitHub, and others). The tab is available for artifacts in proxy repositories, except Docker and raw. The cache duration for this data retrieved from the Index service is configured by SECURITY_INFO_CACHE_TTL_HOURS.

The Security report tab in the artifact details

Browsing the Tree

The tree shows the structure of the repository — folders and files with icons. Files with a "lock" icon are protected (locked/release) artifacts that cannot be deleted or overwritten without extra steps. Folders are expanded lazily: children are fetched on click.

Clicking a file opens its details in the right pane: name, size, path, checksum, and format-specific metadata (Maven groupId/artifactId/version, Docker tags, and so on).

Search, Sort, and Filters

Above the tree is the Search artifacts field: start typing a substring — the tree is replaced with a flat list of matching artifacts (name and path). Clicking a row opens the details of the matching artifact.

Two buttons sit next to the search field.

The Sort button controls how the tree is ordered:

  • Sort by — Name, Date added, or Date modified;
  • Sort direction — A to Z or Z to A.

The filter button (funnel icon), available for proxy repositories, exposes a Show uncached artifacts toggle. By default the tree shows only what is already in the local cache. With the toggle enabled, Save also lists artifacts known to upstream but not yet cached — useful for browsing what's available in the proxied source without triggering a real pull. This is not available for every repository format, because the underlying protocols differ.

Actions on a Single Artifact

After selecting an artifact, the header of the right pane offers the Download button and, in the three-dot menu next to it, Lock artifact and Delete artifact:

  • Download — saves the file through the browser's regular download. Only files can be downloaded, not folders; for an artifact that is not cached yet, the button is disabled;
  • Lock artifact — marks the artifact as release. Once locked, the artifact cannot be deleted or overwritten by a normal upload — this protects released versions from being silently replaced;
  • Delete artifact — removes the file from the repository (for hosted) or from the cache (for proxy). Unavailable for a locked artifact.
Lock is irreversible in the UI

The release flag can only be cleared through the API: PUT /api/v1/artifacts/release?id=<artifact-id> with {"is_release": false}.

Bulk Actions

Each tree row has a checkbox on its left. Once at least one checkbox is ticked, a panel appears above the tree with the number of selected items, Select All, and the actions:

  • Lock — locks the selected artifacts in bulk;
  • Delete — deletes them in bulk.

Both actions ask for confirmation; afterwards, a notification shows how many items were processed.

Connecting Clients and Authentication

Save supports several authentication methods to cover both interactive clients and CI pipelines. The client chooses the method itself through the Authorization header (or a format-specific header).

MethodHeadercs-auth typeWhen to use
Basic Auth (user)Authorization: Basic base64(<username>:<password>)basicInteractive work from IDEs, manual curl, mvn/pip/npm runs as a specific user
Basic Auth (robot account)Authorization: Basic base64(sa$<robot-name>:<api-key>)api_keyCI/CD pipelines, build runners, and any service integration
Bearer JWTAuthorization: Bearer <jwt>bearer_jwtSession requests from the web interface and other services after login via cs-auth
Bearer (opaque token)Authorization: Bearer <opaque-token>npm_tokenAny non-JWT bearer token; in practice, the npm client after npm adduser / npm login
X-NuGet-ApiKeyX-NuGet-ApiKey: <api-key>nuget_keyNuGet format only, for compatibility with dotnet nuget push
Docker v2 tokenAuthorization: Bearer <docker-jwt>docker_tokenOCI clients after going through the 401 → retry → 200 cycle

A Bearer token with three dot-separated segments is classified as a JWT; any other Bearer token is treated as an opaque npm token. The sa$ prefix in Basic Auth marks a service account (robot).

Getting an Admin JWT

The administrative examples below use the Authorization: Bearer $ADMIN_JWT header. cs-auth issues an access token in exchange for the administrator's username and password, and that token lives for 15 minutes by default.

Step 1. Find the administrator password

The admin account is created automatically on first startup. Where its password comes from depends on how the service was deployed:

SituationWhere to get the password
The AUTH_ADMIN_PASSWORD environment variable is setThe value of that variable. No password change is required at first login
The variable is not setThe password is generated randomly and printed to the cs-auth log once, on first startup. The system forces you to change it.
The AUTH_ADMIN_PASSWORD_FILE variable is setThe same generated password is additionally written to the given path with mode 0600

The generated password is printed as a separate block that is easy to find in the logs:

************************************************************************
CodeScoring.Save — INITIAL ADMIN CREDENTIALS (shown once)
  username: admin
  password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
You will be required to change this password on first login.
************************************************************************
# Kubernetes
kubectl logs deploy/cs-auth | grep -A 3 "INITIAL ADMIN CREDENTIALS"
The password is shown only once

The block is printed only during the startup that created the account. If the logs from that first start have already rotated and AUTH_ADMIN_PASSWORD_FILE was never set, the password cannot be recovered.

Step 2. Obtain an access token

curl -sS -X POST https://save.example.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "<admin-password>"}'

The response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "a1b2c3d4e5f6...",
  "token_type": "Bearer",
  "expires_in": 900,
  "must_change_password": false,
  "permissions": {
    "uid": 1,
    "usr": "admin",
    "adm": true,
    "g": ["users.create", "users.list", "config.update", "..."]
  }
}

For convenience, store the token in an environment variable - every example below uses exactly this one:

export ADMIN_JWT=$(curl -sS -X POST https://save.example.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "'"$ADMIN_PASSWORD"'"}' | jq -r .access_token)

Step 3. Change the password if required

If the response contains must_change_password: true, you have a token but almost everything is closed off: any /api/v1/admin/* endpoint returns 403 with the body {"code": "password_change_required"}. Until the password is changed, only GET /api/v1/auth/me, PUT /api/v1/auth/me/password, and the login / refresh / logout endpoints themselves are available.

curl -sS -X PUT https://save.example.com/api/v1/auth/me/password \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -d '{"old_password": "<current-password>", "new_password": "<new-password>"}'

The new password must be at least 8 characters long. The must_change_password flag is baked into the already-issued token, so you have to log in again after changing the password (step 2) — the old token stays restricted until it expires.

Step 4. Verify the permissions

curl -sS https://save.example.com/api/v1/auth/me \
  -H "Authorization: Bearer $ADMIN_JWT"

The response should contain "is_admin": true — an administrator passes every permission check.

Refreshing the token

The access token lives for 900 seconds (configurable through AUTH_JWT_EXPIRY), and the opaque refresh token for 7 days (AUTH_REFRESH_TOKEN_TTL). Once the access token expires, obtain a new pair without re-entering the password:

curl -sS -X POST https://save.example.com/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "<refresh-token>"}'

Refresh tokens are rotated: the presented token is revoked and a new one comes back in the response — store it on every iteration. No Authorization header is needed for this request.

Do not use the administrator account in CI

The administrator's password and its short-lived token are not meant for pipelines. For automation, create robot accounts with a narrow permission set — their keys are not tied to an employee and appear in the audit log under their own identity.

Robot Accounts for CI/CD

Personal passwords are not recommended for CI/CD integrations — they're tied to an employee and rotate often. The right tool is a robot account: a service account with its own role and a long-lived API key.

Creating Through the Web Interface

  1. In the sidebar, open Settings -> Robot accounts.
  2. Click Create robot account in the top-right corner.
  3. Fill in the fields:
    • Login — the service name used for authentication (for example, ci-builder). Save automatically adds the service-account prefix to the value you enter;
    • Name — a human-readable name (for example, CI Builder Bot);
    • Description — an optional description;
    • Expires in — the date when the key expires. Quick presets Week, Month, and Year are available in the date picker. To make the key permanent, tick the Never checkbox to the right of the date picker — the date input is then disabled automatically;
    • Global permissions — the robot's global permissions;
    • Scoped permissions — permissions limited to one, several, or all projects or repositories. The Scope switch selects the level (Projects / Repositories), and each scope adds entries through Add project permissions / Add repository permissions.
  4. Click Create robot account.

When the robot is created successfully, a modal window with the API key opens automatically — this is the password used for Basic Auth.

The API Key Window

After clicking Create robot account, Save opens the API key created window. The modal shows:

  • API key — a read-only field with the key itself and a copy button that puts it on the clipboard.
  • Warning — server-supplied text, for example Store this API key securely — it cannot be retrieved again.

Below the secret is an I have copied the key button that closes the window and takes you to the newly created robot account's edit page.

The API key is shown only once

After the window closes, the same key cannot be viewed or retrieved again — Save stores only its hash. Copy the key and save it into your CI secret storage (GitLab CI variables, GitHub Actions secrets, HashiCorp Vault) before closing the window. If the key is lost, rotate it via POST /api/v1/admin/robots/<id>/api-keys/rotate (the web interface has no key rotation yet) — once rotated, the old value becomes invalid.

Creating Through the API

This call requires an administrator token in the ADMIN_JWT variable (see Getting an Admin JWT).

curl -X POST https://save.example.com/api/v1/admin/robots \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -d '{
    "username": "ci-builder",
    "display_name": "CI Builder Bot",
    "description": "Robot account for CI/CD pipelines",
    "permissions": {
      "project": {
        "backend-team": ["project_artifacts.download", "project_artifacts.upload"]
      }
    },
    "key_expires_in_seconds": 7776000
  }'

The response returns api_key, which is exposed only once at creation time — save it in your CI secret storage.

Client Usage

username = sa$<robot-name>, password = <api-key> — standard Basic Auth:

curl -u "sa\$ci-builder:<api-key>" https://save.example.com/...
Escaping

$ in shell In bash, $ inside single quotes is preserved as-is; inside double quotes it must be escaped as \$. No escaping is needed in YAML / TOML / XML configuration files.

NuGet API key — implementation detail

The X-NuGet-ApiKey header is supported directly: cs-auth classifies it as nuget_key and validates it by the 12-character key prefix. Both options work for NuGet, but we recommend Basic Auth with a robot account — it is consistent with the other formats, and the robot's identity is recorded explicitly in the audit log.

Anonymous Read Access

When the global AllowAnonymousRead flag is enabled, unauthenticated requests receive a PermissionSet granting projects.view, project_repos.view, project_artifacts.view, and project_artifacts.download across all projects. For Docker / OCI this mode still requires the 401 → /v2/token → 200 cycle — otherwise the Docker client cannot work (see Working with OCI / Docker).

The current value of the flag is visible under Settings -> Configuration. Changing it is done through the API. Every call below requires an administrator token (see Getting an Admin JWT).

curl -sS -X PUT https://save.example.com/api/v1/admin/config \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -d '{"auth.allow_anonymous_read": "true"}'

Upstream Access for Proxy Repositories

Upstream Credentials

If an external repository requires authentication (a private Nexus, Artifactory, GitLab, Private Packagist, etc.), pass the credentials in the auth block when creating or updating the proxy repository:

curl -X POST https://save.example.com/api/v1/repos \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "project": "backend-team",
    "name": "nexus-maven-proxy",
    "format": "maven",
    "repository_type": "proxy",
    "remote_url": "https://nexus.example.com/repository/maven-public/",
    "auth": {
      "auth_type": "basic",
      "username": "<upstream-user>",
      "password": "<upstream-password>"
    }
  }'
auth_typeWhat to fill inWhat upstream receives
none—Requests without authentication (the default)
basicusername, passwordAuthorization: Basic ...
bearertokenAuthorization: Bearer <token>
apikeytokenNuGet only: the X-NuGet-ApiKey header

The password and the token are write-only: the API never returns them and reports the has_password and has_token flags instead. To change the credentials, send a new auth block in PUT /api/v1/repos/<repository_id>. If the credentials do not fit the format (for example, apikey for a format other than NuGet), Save logs the warning Configured upstream credential could not be applied; requests proceed anonymously and accesses upstream without authentication.

Where Save Sends Credentials

Save makes sure upstream credentials do not reach third parties:

  • Only to the upstream host. The credentials are sent only to the host from remote_url and its subdomains. If upstream redirects a download to another host (CDN, S3), Save removes Authorization and X-NuGet-ApiKey from the request and downloads the file anonymously. For Composer, archives from GitHub are also downloaded without the upstream credentials.
  • No https to http downgrade. If upstream is served over https and redirects to http while the request still carries credentials, Save does not follow the redirect. The request fails, and the log shows refusing to follow redirect from https to ... with upstream credentials attached. There are no retries: this is an upstream configuration error.
  • Docker: token service over https only. If a registry is served over https but its token service (the realm from the WWW-Authenticate header) uses http, Save does not send the password there. The log shows refusing to send upstream credentials to non-https token realm.
  • Upstream over http. A proxy repository with an http:// remote_url and credentials works, but the password is sent in clear text. Save logs the warning upstream credentials are configured over plain http and are sent unencrypted once per host. Use https whenever possible.

For Docker registries that issue Bearer tokens (Docker Hub, Harbor, and so on), listing upstream images works correctly as well.

Certificates and Corporate Proxy

  • Certificates. Save verifies the upstream certificate against the system root certificate store of the container. If upstream is signed by a corporate certificate authority, mount its certificate into the container (for example, into the /extra-ca directory) and set SSL_CERT_DIR=/etc/ssl/certs:/extra-ca — this keeps the system certificates trusted as well. Do not use SSL_CERT_FILE for this: it replaces the whole system store. Save does not retry certificate errors (unknown authority, expired certificate, wrong host name): the client gets an error immediately, and the cause is visible in the backend log.
  • Corporate proxy. All proxy repositories, including NuGet, honor the HTTPS_PROXY, HTTP_PROXY, and NO_PROXY variables. Add the internal addresses that Save must reach directly to NO_PROXY: the cs-auth service, S3/MinIO, internal upstreams.

Cleanup Policies

To keep storage from growing without bound, repositories can have cleanup policies attached — rules for automatic deletion (or retention) of artifacts. Save supports five policy types; simple ones are enough for most cases, while complex AND/OR conditions use the expression type.

Creating a Policy

  1. In the sidebar, open Cleanup.
  2. Click Create policy.
  3. Fill in the common fields: Display name, Description, and optionally Schedule (a five-field cron expression: minutes, hours, day, month, weekday).
  4. Choose Policy type — this defines the main cleanup rule:
Policy typePurposevalue parameter
delete-snapshots-older-thanDelete SNAPSHOT/dev versions older than N daysnumber of days, for example 30
keep-latest-versionsKeep only the N latest versions of each artifact, delete the restN, for example 5
delete-by-ageDelete any artifact older than N daysnumber of days
delete-by-sizeDelete the oldest artifacts once a size limit is exceededsize in gigabytes as an integer, for example 50
expressionFlexible DSL rule combining criteria with AND/ORan expression object is passed instead of value
  1. Specify the Formats the policy applies to. Leaving this empty means it applies to every format.
  2. For the expression type, build the criteria tree as well — each criterion combines a type, a comparison operator (eq, ne, gt, lt, matches, contains), and a value; criteria can be grouped with logical operators AND or OR.
  3. Click Create policy.

Available Criterion Types for expression

Full list: GET /api/v1/enums/cleanup-criterion-types.

GrouptypePurpose
Agecreated_beforeArtifact age (in days)
last_downloaded_daysDays since the last download
not_downloaded_sinceNever downloaded, or not downloaded since the given date
Versionversion_patternGlob pattern matched against the version
is_snapshot, is_prerelease, is_releaseSnapshot / pre-release / release markers
Name/pathname_pattern, path_patternGlob patterns
Sizesize_greater_than, size_less_thanArtifact size
Dockerdocker_tag, docker_untaggedTag / no tag
Mavenmaven_classifier, maven_packagingClassifier / packaging
npmnpm_scopePackage scope
PyPIpypi_package_typesdist, bdist_wheel, …
NuGetnuget_is_prereleaseNuGet pre-release flag
OCIoci_artifact_typeOCI artifact type (Helm, Cosign, SBOM, …)
Retentionkeep_latestKeep N latest versions

Assigning a Policy to a Repository

The assignment happens on the create or edit form of a repository (or of a project — in which case the policy cascades to every repository inside the project).

  1. Open the repository and click the pencil button (Edit repository) in the header.
  2. Scroll down to the Cleanup policy section.
  3. The left column (Available) lists every existing policy; the right column (Applied) lists the ones already attached. Tick the policies you need and move them to Applied with the arrow; to detach a policy, remove it from the Applied list.
  4. Save the changes.

Manual Run and Preview via API

# Dry run — see what would be deleted, without saving the policy
curl -X POST https://save.example.com/api/v1/cleanup/preview \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "repository_id": 42,
    "policy_type": "delete-snapshots-older-than",
    "value": "30"
  }'
# Run saved policies (with an optional dry_run)
curl -X POST "https://save.example.com/api/v1/cleanup/execute?project=backend-team" \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "policies": ["delete-old-snapshots", "keep-latest-5"],
    "dry_run": true
  }'

Access Management

Users, roles, and access rights for projects and repositories are managed by a separate service, cs-auth. Its API is proxied through Save under /api/v1/auth/* and /api/v1/admin/*. The authentication methods are described above in Connecting Clients and Authentication.

In the web interface, the relevant sections live under Settings:

  • Roles — role definitions with their permission sets;
  • Users — human accounts;
  • Robot accounts — service accounts (see above).
cs-auth API

The exact endpoints for managing users, roles, and project membership are part of the cs-auth API surface. Save forwards them transparently. They are described in Swagger UI in a separate document, CodeScoring.Auth API (see API Documentation (Swagger)).

Access Levels

Permissions in CodeScoring.Save are granular and follow the <resource>.<action> format. Roles in cs-auth are user-defined and consist of such permissions.

Repository level (artifacts inside a repository):

PermissionAllows
artifacts.viewViewing the artifact list and metadata
artifacts.downloadDownloading artifacts
artifacts.uploadPublishing artifacts (relevant for hosted)
artifacts.deleteDeleting artifacts
artifacts.lockLock/unlock one or all artifacts

Repository level (the repository itself):

PermissionAllows
repos.viewViewing repository settings and statistics
repos.editChanging repository settings
repos.deleteDeleting the repository

Project level (cascade permissions that apply to every repository and artifact in the project):

PermissionAllows
projects.view, projects.create, projects.edit, projects.deleteManaging projects
project_repos.view, project_repos.create, project_repos.delete, project_repos.editManaging all repositories in a project
project_artifacts.view, project_artifacts.upload, project_artifacts.download, project_artifacts.delete, project_artifacts.lock, project_artifacts.unlockAll artifact operations in any repository of the project

Global (only available through a global-scope role):

PermissionAllows
cleanup.view, cleanup.edit, cleanup.deleteManaging cleanup policies
roles.create, roles.deleteManaging roles
users.create, users.deleteManaging users
config.view, config.manageViewing and editing configuration
audit.viewViewing the audit log
Cascade logic

Project-scope permissions automatically cascade to the repository scope: a user with project_artifacts.download on a project gets artifacts.download on every repository inside that project. The * key grants wildcard access to all projects / repositories.

Assigning a User or Robot Account to a Project

Membership ties a user or a robot account to a project with a specific role. The assigned member receives every permission of the role and shows up as a member in the project header.

Through the Web Interface

Section in progress

The Members section on the project page is still being built. The backend supports every necessary operation (/api/v1/admin/projects/<project>/members), but the Members tab is not yet exposed in the project UI.

Until the tab is available, use the API operations below.

Through the API

Every call below requires an administrator token in the ADMIN_JWT variable (see Getting an Admin JWT).

Adding a member:

curl -X POST https://save.example.com/api/v1/admin/projects/backend-team/members \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -d '{
    "user_id": 5,
    "role_id": 2
  }'

For human users role_id is required; for robot accounts (is_service=true) it can be omitted.

Listing members:

curl -sS -H "Authorization: Bearer $ADMIN_JWT" \
  "https://save.example.com/api/v1/admin/projects/backend-team/members?limit=50"

Changing the role of an existing member:

curl -X PUT https://save.example.com/api/v1/admin/projects/backend-team/members/5 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -d '{"role_id": 3}'

Removing a member from the project:

curl -X DELETE https://save.example.com/api/v1/admin/projects/backend-team/members/5 \
  -H "Authorization: Bearer $ADMIN_JWT"

The Same Actions via API

Every action covered above for the web interface is also available through the REST API. This is useful for bootstrap scripts, infrastructure-as-code (Terraform / Ansible), and integration tests.

API Documentation (Swagger)

The complete API description opens in Swagger UI:

https://save.example.com/api/swagger/

Two documents are switched in the top-right corner: CodeScoring.Save API (repositories, artifacts, cleanup policies, activation key) and CodeScoring.Auth API (users, roles, robot accounts, audit). The specifications themselves are available at /api/swagger/doc.json and /api/v1/admin/swagger/doc.json — they are convenient for generating clients. In the web interface, Swagger UI opens with the CS Save API button at the bottom of the sidebar. To call a method directly from Swagger UI, click Authorize and paste Bearer <token> (see Getting an Admin JWT).

Creating a Project

curl -X POST https://save.example.com/api/v1/projects \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "name": "backend-team",
    "display_name": "Backend Team",
    "description": "Backend services and microservices",
    "color": "#4A90D9"
  }'

Creating a Proxy Repository

curl -X POST https://save.example.com/api/v1/repos \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "project": "backend-team",
    "name": "maven-central",
    "display_name": "Maven Central (proxy)",
    "format": "maven",
    "repository_type": "proxy",
    "remote_url": "https://repo1.maven.org/maven2/",
    "cache_ttl": 86400
  }'

Creating a Hosted Repository

curl -X POST https://save.example.com/api/v1/repos \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "project": "backend-team",
    "name": "internal-maven",
    "display_name": "Internal Maven",
    "format": "maven",
    "repository_type": "hosted"
  }'

Assigning a Cleanup Policy

curl -X POST "https://save.example.com/api/v1/cleanup/assignments?project=backend-team" \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "policies": [
      {"policy_name": "delete-old-snapshots", "priority": 10, "enabled": true}
    ]
  }'

Creating a Cleanup Policy

curl -X POST https://save.example.com/api/v1/cleanup/policies \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "name": "delete-old-snapshots",
    "display_name": "Delete Old Maven Snapshots",
    "description": "Delete Maven SNAPSHOT versions older than 30 days",
    "policy_type": "delete-snapshots-older-than",
    "value": "30",
    "formats": ["maven"],
    "enabled": true,
    "schedule": "0 2 * * *"
  }'

Creating an Expression-Based Policy

curl -X POST https://save.example.com/api/v1/cleanup/policies \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "name": "clean-old-prereleases",
    "display_name": "Clean Old Pre-releases",
    "policy_type": "expression",
    "formats": ["maven", "npm"],
    "expression": {
      "action": "delete",
      "logical_operator": "and",
      "criteria": [
        {"type": "is_prerelease", "value": "true"},
        {"type": "created_before", "value": "14"}
      ]
    },
    "enabled": true
  }'

Monitoring

Audit Log

The audit log records every change to projects, repositories, artifacts, roles, and robot accounts. In the UI it is available:

  • Globally — under Settings -> Audit log;
  • Per project — through the journal icon on the right of the project header, which opens the audit log filtered to that project.

Each entry contains the event time, the initiator, the resource type and identifier, the action type, and a set of fields with details (the name of the uploaded artifact, changed repository settings, and so on). Entries are grouped by day and displayed as a timeline, and the links inside an entry are clickable (they open the corresponding project, repository, policy, user, or role).

The log can be exported for a specific period through the Export button in the header (formats: JSON or HTML).

# Audit log for a specific repository via API
curl -u "<username>:<password>" \
  "https://save.example.com/api/v1/admin/audit?resource_type=repository&q=maven-central&limit=50"

Repository Statistics

Current aggregated repository metrics are available via the API:

curl https://save.example.com/api/v1/repos/<repository_id>/stats \
  -u "<username>:<password>"

Response:

{
  "id": 42,
  "name": "maven-central",
  "project_name": "backend-team",
  "artifact_count": 1523,
  "total_size": 16413032448,
  "cache_hit_ratio": 0.85,
  "last_activity": "2026-03-17T10:30:00Z"
}
Statistics fields

The endpoint returns aggregated values only. The cache_hit_ratio field is present only for proxy repositories.

Service Status

curl https://save.example.com/health

A global service health check. There are no per-repository health endpoints: upstream availability is checked by background workers and exposed through Prometheus metrics.

Logs

Logs are centralized and emitted as JSON. Filtering by a specific repository is done through standard log-aggregator tools.

Was this page helpful?