Skip to content

Projects ​

A project is the unit of ownership for repositories. Every repository belongs to exactly one project (the built-in system project when none is given), and the people and groups who hold a role in the project get access to all of its repositories. Project owners and maintainers manage membership themselves, without being global admins.

Project access is added to permission targets (union, no deny rules); it never replaces them. How the data-plane access is derived from a role is in RBAC.

Roles ​

Roles rank viewer < developer < maintainer < owner. A user's effective role is the highest of their personal membership (or recorded ownership) and any group grant. A global admin is an implicit owner of every project: my_role reports owner for an admin who holds no role.

Capabilityviewerdevelopermaintainerownerglobal admin only
See the project, its repositories, members and groupsxxxx
Read artifacts in the project's repositoriesxxxx
Write artifactsxxx
Delete artifacts, list and restore repository trashxx
Create repositories in the project (policy applies)xx
Update, delete and configure the project's repositories (cleanup policies, quota, trash emptying)xx
See the project's storage usage, quota and default cleanup policiesxxxx
See the project's activity (audit events of the project and its repositories)xxxx
Move a repository to another project (owner of both)x
Create, change and delete the project's default cleanup policiesx
Add, change and remove members and groups below maintainerxx
Grant, change or remove maintainer and ownerx
Edit display_name and descriptionx
Delete the project (when it owns no repositories)x
Create a project with a chosen owner or policy fieldsx
Set the project's storage quota (ceiling over all its repositories)x
Change policy fields (allowed_formats, allowed_repo_types, repo_name_prefix, allow_public_repos, max_repositories, token_max_ttl_days)x

Project roles never grant the global manage permission: users, groups, permission targets, settings and every other /api/v1/* route outside /api/v1/projects/** stay admin-only. The one exception is the delegated repository administration of the table above: for a repository owned by a (non-system) project, a maintainer or owner of that project passes the checks of PATCH and DELETE /repository/{name}, /repositories/{name}/cleanup-policies[...], GET, PUT, DELETE /repositories/{name}/quota, GET /repositories/{name}/members and DELETE /repositories/{name}/trash. This is a separate check (repo.admin): it is never satisfied by a repository-scoped manage permission target, and it is never granted by a scoped, deploy or anonymous credential. A caller who cannot even read the repository gets 404. Webhooks, quota reconcile, group-member management and every other repository sub-route stay admin-only. Repository responses report the caller's permissions.admin.

Repository self-service ​

POST /projects/{project}/repositories takes the body of POST /repositories (project is ignored; the path decides) and creates the repository in the project, with the creator needing no permission target: the project roles give access. For non-admins the project policy is enforced:

Policy fieldViolation
repo_name_prefixname must start with it: 400
allowed_formats, allowed_repo_types400
allow_public_repos falsepublic: true is 403
max_repositories409 project_repository_limit (checked under a lock on the project row, so concurrent creates cannot overshoot)
group member_reposeach member must be readable by the caller: 403

Global admins bypass the policy, on both routes. A non-admin PATCH /repository/{name} may change description, retention, immutability and the upstream settings but not project, blocked, presign_downloads or a group's merge_proxy_versions (dependency-confusion protection, global admins only). Changing a proxy's upstream_url origin or upstream_username clears the stored upstream password unless a new upstream_password is sent in the same request. A non-admin may not make the repository public unless the project allows it (403).

POST /projects/{project}/repositories/{name}/transfer with {"to_project": "<name>"} moves a repository between projects. The caller must be owner of both (admins always); a caller with no role in the target gets 404, a lower role 403. A non-admin transfer must also satisfy the target's policy (prefix, format, type, visibility, max_repositories). The repository's storage usage moves with it: it is subtracted from the source project's counters and added to the target's, and the transfer fails with 409 project_quota_exceeded when the target's project quota cannot take it.

Group grants take viewer, developer or maintainer; owner is personal and cannot be given to a group. The built-in system project grants nothing and cannot be deleted (409).

Escalation rules ​

Enforced on every member and group mutation, under a lock on the project row so concurrent requests cannot break them:

  • A maintainer can only add, change or remove roles below maintainer, for members and for groups. Granting maintainer or owner, or touching a user or group that already holds one of them, needs an owner.
  • Nobody can grant a role above their own.
  • The project's recorded owner and its last owner cannot be demoted or removed (409).
  • Groups never get owner.

404 and 403 ​

On /api/v1/projects/{project}/**:

CallerResult
not authenticated401
not a member (or the project does not exist)404, identical in both cases, so project names do not leak
a member whose role is too low for the action403
anonymous callers, deploy tokensdenied, never a project role

Tokens with non-empty scopes get no project role (scopes are permission-target IDs; they only narrow). A token without scopes acts with its owner's roles.

Sub-paths and methods that are not listed in the API table below fall back to the global manage permission (deny by default), so a project owner cannot reach an undocumented route.

Creating projects ​

By default only a global admin creates projects (POST /api/v1/projects, oa project create, or New project on the Projects page). The admin may name the owner (a username, default: the caller) and set policy fields in the same request.

Setting projects.self_create lets every authenticated human user create a project and become its owner. Sessions and access tokens without scopes qualify; deploy tokens, the legacy key and scoped tokens do not.

WhereHow
Config fileprojects.selfCreate: true (openartifact.example.yaml)
EnvironmentOPENARTIFACT_PROJECTS_SELF_CREATE=true
Admin UIthe self-service toggle in Settings > Project creation policy (/settings/project-policy)
APIGET / PUT /api/v1/admin/projects/settings with {"self_create": true} (global admin)

The config value or environment variable is the default until an admin saves the setting through the UI or API; a saved value takes precedence. If the stored value cannot be read, self-creation is off.

A self-created project:

  • is owned by the creator, who is added as owner;
  • accepts only name, display_name and description; a request with owner or any policy field is answered with 403;
  • starts with the default policy: hosted repositories only, all formats, repository name prefix <slug>-, no public repositories, at most 50 repositories, token TTL cap of 365 days. The owner cannot change these; a global admin can. The policy is enforced when non-admins create or transfer repositories.

GET /api/v1/projects returns can_create (admin, or self-creation on) so clients can show or hide the create action. Each project of the list also carries member_count (direct members), usage.size_bytes (the sum of its repositories' counters) and, when a size limit is set, quota.limit_bytes, computed for the whole list with grouped queries; q keeps the projects whose name, slug or display name contains it.

Project tokens ​

A project token is a CI credential that belongs to one project. Each token is a deploy token of its own service account (a hidden user that cannot log in, is never listed with users or members, and holds exactly the token's role in the project):

  • viewer tokens read the project's repositories, developer tokens also write. Other projects, permission targets and the whole management API (/api/v1/**, including /auth/me) are unreachable with them: the same token against another project's repository gets 403.
  • Minting needs a session (an API token cannot mint tokens). The role may not exceed the creator's own role and is capped at developer; global admins count as owners.
  • The expiry is required when the project limits token lifetime (token_max_ttl_days) and may not exceed it.
  • A project has at most 100 active tokens, and active names are unique per project (409 otherwise). A revoked token's name can be reused.
  • Maintainers, owners and admins list and revoke every token of the project; other members only their own. Revocation is immediate on every replica that shares the database (the auth cache is invalidated with the write).
  • When a token's creator is removed from the project, disabled or deleted, all their tokens in that project are revoked in the same transaction. Access a creator holds through a group stays a membership, so remove the group grant too.
  • Expired and revoked tokens and their service accounts are purged by the auth purge worker.
  • Project tokens are not included in backups: after a restore, mint them again.

project.token_created and project.token_revoked are audited with the token as the resource. The admin token list (GET /api/v1/admin/tokens) shows project, project_role and created_by for project tokens.

Storage quota and cleanup defaults ​

A project can carry a storage quota: a ceiling on the total size and artifact count of all the repositories it owns, set by a global admin (PUT /projects/{project}/quota; owners and maintainers can read it, not change it). It sits on top of the per-repository quotas: an upload must fit the repository's own limit and the project's, and a repository limit cannot exceed the project limit (setting one above it is 400 validation_failed; 0, unlimited, is always accepted and the project ceiling then bounds the repository). Exceeding either is 507 Insufficient Storage with code quota_exceeded. The rules, the counters and the reconcile are described in Cleanup policies and storage quotas.

Members see the numbers with GET /projects/{project}/usage, or in the Usage section of the project page: totals, the project quota against its warning threshold and one line per repository. Proxy and group repositories hold no uploaded packages and are shown as not metered.

A project can also define default cleanup policies (/projects/{project}/cleanup-policies; members read, owners and admins write). A default policy applies to every hosted repository of the project that has no enabled cleanup policy of its own; a repository's own enabled policy overrides the defaults. Details: project default cleanup policies.

In the web UI the project quota (editable by global admins, read-only for everyone else) is in the Usage tab and the cleanup defaults (listed for all members, editable by owners and admins) in the Settings tab of the project.

Audit events ​

Every project mutation writes an audit entry with the project in project_id:

ActionWhen
project.created, project.updated, project.deletedproject lifecycle and display_name / description changes
project.policy_changeda policy field changed (admin)
project.member_added, project.member_removed, project.member_role_changedmembership changes
project.group_added, project.group_removed, project.group_role_changedgroup grants
project.token_created, project.token_revokedproject tokens

project.deleted carries the project ID as the resource ID instead of project_id: the audit row's project reference is cleared when the project goes away. Changing the self-creation setting is recorded as system.setting_changed. Read the whole log with GET /api/v1/admin/audit or oa audit list (global admins).

Project activity ​

Every member reads the events of their own project, without being a global admin, with GET /projects/{project}/activity or in the Activity tab of the project page. The feed holds the entries whose project_id is the project (the table above) plus the entries recorded against a repository the project owns today (repository_id): project.repository_created, project.repository_deleted, project.repository_transferred, and the repository events of the trash (artifact.restored, artifact.purged, repository.restored, repository.purged). A repository moved to another project takes its entries with it. Uploads and downloads are not audit events (see monitoring), so the feed shows administrative changes, not package traffic. project.token_created and project.token_revoked are left out of the feed for viewers and developers (token names and roles are maintainer-level information); maintainers, owners and global admins see them.

  • Newest first, keyset paging like the admin log: per_page (default 25, at most 100), cursor (the previous next_cursor), has_more. There is no total and no row is ever counted. actor and action are case-insensitive substring filters.
  • Entries carry id, timestamp, action, actor (user or token name), actor_type, message, resource_type, resource_id and repository_id. They never carry the client IP address: that stays in the admin log.
  • Entries also carry actor_name and subject (the same resolved names as the admin audit log). GET /repositories/{name}/activity?package_name=&version=&cursor=&limit= returns the same entries for one repository (admins and project maintainers): the events recorded against it, which are the trash events, promotions and the project's repository events. Uploads are not audit events with a repository, so publishes do not appear. package_name and version match only events that record a package version (package deletes, restores and purges).
  • GET /projects/{project}/activity/export?format=jsonl|csv downloads the same feed (all of it, optionally narrowed by since, until, actor, action) with the same visibility rules; fields and format in audit export. The Activity tab has the matching buttons.
  • Access is the viewer role: a non-member gets 404, the same as for an unknown project, so one project's activity never leaks into another's. Anonymous, deploy and scoped credentials get no project role, as for every other project route.

API ​

All paths are under /api/v1, JSON is snake_case, errors are {"error": message, "code": code}. {project} is the project name. Minimum role in the second column; "admin" means global admin.

RouteWhoNotes
GET /projectsany authenticated user{projects, can_create}: the caller's projects with my_role; ?all=true is admin-only
POST /projectsadmin, or any human user with projects.self_createbody name, display_name, description; admin may add owner and policy fields
GET /projects/{project}viewerincludes my_role, repository_count, is_system and the policy fields
PATCH /projects/{project}ownerdisplay_name, description; policy fields need an admin (403 for an owner)
DELETE /projects/{project}owner204; 409 while it owns repositories and for the system project
GET /projects/{project}/repositories?page=&limit=viewer{repositories, total, page, limit}, live repositories, newest first
POST /projects/{project}/repositoriesmaintainerbody of POST /repositories; 201; policy errors 400/403, 409 project_repository_limit
POST /projects/{project}/repositories/{name}/transferowner (and owner of the target)body {to_project}; 404 for a repository of another project
GET /projects/{project}/membersviewereach member has user_id, username, role
POST /projects/{project}/membersmaintainerbody user_id or username (exact), plus role (default viewer); 201
PATCH /projects/{project}/members/{user_id}maintainerbody {role}
DELETE /projects/{project}/members/{user_id}maintainer204
GET /projects/{project}/groupsviewer
PUT /projects/{project}/groups/{group}maintainer{group} is a group id or its exact name; body {role} (viewer, developer, maintainer)
DELETE /projects/{project}/groups/{group}maintainerid or name
GET /projects/{project}/tokensviewermaintainers see all tokens, others their own; {tokens} without secrets
POST /projects/{project}/tokensviewersession only; body name, role (viewer, developer, at most your own), expires_at; 201 with the secret in token, shown once; 409 on duplicate name or 100 active tokens
DELETE /projects/{project}/tokens/{token_id}viewermaintainer for tokens of others; 204
GET /projects/{project}/usageviewer{project, size_bytes, artifact_count, quota, repositories}: totals, the project quota and one line per live repository (name, format, repo_type, size_bytes, artifact_count, repository limits, quota_enforced)
GET /projects/{project}/activity?per_page=&cursor=&actor=&action=viewer{data, per_page, has_more, next_cursor}: audit events of the project and its repositories, newest first, keyset paging only (see Project activity)
GET /projects/{project}/quotaviewerthe project quota: max_size_bytes, max_artifact_count, warn_threshold_percent, current_size_bytes, current_artifact_count, usage_percent, enforced, updated_at
PUT /projects/{project}/quotaadminbody max_size_bytes, max_artifact_count, warn_threshold_percent (omitted fields keep their value, 0 = unlimited); owners get 403, a negative limit or a threshold outside 0-100 is 400 validation_failed
GET /projects/{project}/cleanup-policiesviewer{policies}, the project default policies
POST /projects/{project}/cleanup-policiesownersame body and validation as a repository cleanup policy; 201
PUT, DELETE /projects/{project}/cleanup-policies/{id}ownerpartial update; delete is 204
GET, PUT /admin/projects/settingsadmin{self_create: bool}

GET /api/v1/auth/me lists the caller's own roles in projects: [{name, role}] (admins get no implicit entries there).

From the CLI ​

sh
oa project create platform --display-name "Platform team"
oa project show platform                      # includes my_role
oa project update platform --description "Build and release tooling"
oa project add-member platform --username alice --role developer
oa project set-role platform <user-id> maintainer
oa project add-group platform "ci users" --role developer   # group name or id

The full command list is in the CLI reference.

Free software under the AGPL-3.0-or-later.