1
0
mirror of https://github.com/goharbor/harbor.git synced 2026-10-23 10:08:57 +03:00

6485 Commits

Author SHA1 Message Date
stonezdj(Daojun Zhang)
533cfd7659 fix(replication): preserve artifact type for multi-tier accessories (#24200)
* fix(replication): preserve artifact type for multi-tier accessories

In getAccessoryArts, reassigning art when fetching accessory details
shadowed the parent artifact and caused sub-accessories (such as signatures
of an SBOM) to inherit Type "UNKNOWN" from the accessory artifact rather
than the subject artifact's type (e.g. "IMAGE"). Consequently, when a
replication policy configured a resource filter (e.g. "image"), the
sub-accessories were dropped by the artifact type filter.

Fix this by using a separate variable subArt to avoid shadowing art,
and propagate art.Type to subArt so nested accessories inherit the
root subject artifact's type.

Signed-off-by: stonezdj <stonezdj@gmail.com>

* test(replication): cover nested accessory type inheritance

Signed-off-by: stonezdj <stonezdj@gmail.com>

---------

Signed-off-by: stonezdj <stonezdj@gmail.com>
2026-10-23 03:03:30 +00:00
dependabot[bot]
d1a63a9161 chore(deps-dev): bump shell-quote from 1.10.0 to 1.12.0 in /src/portal (#24112)
Bumps [shell-quote](https://github.com/ljharb/shell-quote) from 1.10.0 to 1.12.0.
- [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/shell-quote/compare/v1.10.0...v1.12.0)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-version: 1.12.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-22 16:59:09 +00:00
dependabot[bot]
65f4244575 chore(deps): bump hono from 4.13.8 to 4.13.13 in /src/portal (#24194)
Bumps [hono](https://github.com/honojs/hono) from 4.13.8 to 4.13.13.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.13.8...v4.13.13)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.13.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Orlix <7236111+OrlinVasilev@users.noreply.github.com>
Co-authored-by: Vadim Bauer <vb@container-registry.com>
2026-10-22 16:07:58 +00:00
dependabot[bot]
63d7c8d5ef chore(deps): bump proxy-addr from 2.0.7 to 2.0.8 in /src/portal (#24110)
Bumps [proxy-addr](https://github.com/jshttp/proxy-addr) from 2.0.7 to 2.0.8.
- [Release notes](https://github.com/jshttp/proxy-addr/releases)
- [Changelog](https://github.com/jshttp/proxy-addr/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/proxy-addr/compare/v2.0.7...v2.0.8)

---
updated-dependencies:
- dependency-name: proxy-addr
  dependency-version: 2.0.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-22 13:34:14 +00:00
dependabot[bot]
0fa571cefb chore(deps): bump ip-address from 10.5.0 to 10.7.4 in /src/portal (#24181)
Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.5.0 to 10.7.4.
- [Release notes](https://github.com/beaugunderson/ip-address/releases)
- [Commits](https://github.com/beaugunderson/ip-address/compare/v10.5.0...v10.7.4)

---
updated-dependencies:
- dependency-name: ip-address
  dependency-version: 10.7.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Orlix <7236111+OrlinVasilev@users.noreply.github.com>
2026-10-22 12:03:57 +00:00
dependabot[bot]
020b3e6c31 chore(deps): bump compression from 1.8.1 to 1.8.2 in /src/portal/app-swagger-ui (#24108)
chore(deps): bump compression in /src/portal/app-swagger-ui

Bumps [compression](https://github.com/expressjs/compression) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: compression
  dependency-version: 1.8.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-22 11:16:14 +00:00
dependabot[bot]
9644fada24 chore(deps): bump shell-quote from 1.10.0 to 1.12.0 in /src/portal/app-swagger-ui (#24106)
chore(deps): bump shell-quote in /src/portal/app-swagger-ui

Bumps [shell-quote](https://github.com/ljharb/shell-quote) from 1.10.0 to 1.12.0.
- [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/shell-quote/compare/v1.10.0...v1.12.0)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-version: 1.12.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-22 09:59:24 +00:00
Prasanth Baskar
f83b3ae751 chore: Upgrade Casbin To v3 (#24189)
Move the RBAC evaluator from github.com/casbin/casbin v1.9.1 to
github.com/casbin/casbin/v3 v3.11.0. v1 is unmaintained and pulls in the
archived Knetic/govaluate.

v3 returns errors from NewEnforcer, Enforce and policy loading; a
failure now denies the request and is logged. v3 pre-registers
keyMatch2 and never replaces it, so Harbor's matcher is registered as
harborKeyMatch2. v3 parses policy lines as CSV, so the adapter loads
rules as arrays to keep names with quotes or commas working.

Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
2026-10-22 15:02:31 +08:00
Prasanth Baskar
4e07c69cbd chore(deps): bump up Go version to v1.27.3 (#24188)
Fixes GO-2026-6809: crypto/x509 now distrusts compromised intermediate
certificates. Affects go1.27.2, fixed in go1.27.3.

Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
2026-10-21 14:28:50 +05:30
jUDASmILE
8414559ce3 Improve project metadata cache consistency (#24139)
* Improve project metadata cache consistency

Signed-off-by: jUDASmILE <judasmile@gmail.com>

* Regenerate cache iterator mock

Signed-off-by: jUDASmILE <judasmile@gmail.com>

* Match generated cache iterator mock ordering

Signed-off-by: jUDASmILE <judasmile@gmail.com>

* address comments

Signed-off-by: jUDASmILE <judasmile@gmail.com>

* fix ut issue

Signed-off-by: jUDASmILE <judasmile@gmail.com>

---------

Signed-off-by: jUDASmILE <judasmile@gmail.com>
2026-10-20 09:32:46 +00:00
Chlins Zhang
a360e527a9 fix(replication): make the image resource filter take effect again (#24176)
The resource filter stopped filtering artifacts by type when the chart
resource type was removed in #18191, so policies with the image filter
replicated charts and other artifacts as well.

Fixes #24047

Signed-off-by: chlins <chlins.zhang@gmail.com>
2026-10-20 06:46:22 +00:00
Qingyuan Wang
dc64f3b4cf portal: align robot account immutability label (#24171)
Use the established “Tag Immutability” label for the robot-account permission selector in all supported locales.

Fixes #24170

Prepared and tested with Codex assistance; no human review is claimed.

Signed-off-by: NUAAwqy <146524024+NUAAwqy@users.noreply.github.com>
2026-10-19 16:12:09 +00:00
stonezdj(Daojun Zhang)
9e79066f74 chore(deps): bump google.golang.org/grpc to v1.82.2 and golang/x/net to v0.60.0 (#24167)
* chore(deps): bump google.golang.org/grpc to v1.82.2

Signed-off-by: stonezdj <stonezdj@gmail.com>

* chore(deps): bump golang.org/x/net to v0.60.0

Signed-off-by: stonezdj <stonezdj@gmail.com>

---------

Signed-off-by: stonezdj <stonezdj@gmail.com>
2026-10-19 12:33:46 +08:00
stonezdj(Daojun Zhang)
f1b4e3ccc6 Fix webhook egress allow all (#24126)
fix(webhook): default webhook egress policy level to allow_all

When the webhook egress policy is empty or unset, default to egressAllowAll
to preserve Harbor's default egress behavior and avoid regressions for internal
and test webhook targets.

Signed-off-by: stonezdj <stonezdj@gmail.com>
2026-10-16 07:06:05 +00:00
stonezdj(Daojun Zhang)
1edae4acdb feat(task): aggregate execution error messages and surface task failure details (#24028)
- Record and bubble up failure error messages (including tail of job logs) in Jobservice
- Store status_message on failed task and aggregate task errors into execution status_message
- Expose status_message in swagger ReplicationTask definition and handler
- Display status_message tooltip on replication tasks and execution status in UI portal

Signed-off-by: stonezdj <stonezdj@gmail.com>
2026-10-16 05:21:50 +00:00
stonezdj(Daojun Zhang)
502a51bdec feat(replication): separate event-triggered replication into dedicate… (#24138)
feat(replication): separate event-triggered replication into dedicated queue

Separate replication tasks generated by event-based executions into
a dedicated EVENT_REPLICATION queue in JobService with higher priority
to prevent head-of-line blocking by bulk manual or scheduled replication.

Signed-off-by: stonezdj <stonezdj@gmail.com>
Co-authored-by: Wang Yan <wangyan_0219@hotmail.com>
2026-10-15 09:55:45 +00:00
jUDASmILE
4cde2fb0e0 Align manual preheat execution with update permissions (#24141)
Signed-off-by: jUDASmILE <judasmile@gmail.com>
2026-10-15 09:07:47 +00:00
Nikita Kodkany
bcea998ed8 fix(jobservice): prevent CPU spin from unreachable cron expressions (#23925)
* fix(jobservice): prevent CPU spin from unreachable cron expressions

Cron expressions like '0 0 3 30 2 *' (Feb 30) parse successfully because
robfig/cron validates each field individually without cross-checking day-of-month
against month. schedule.Next() then returns time.Time{} since no matching date
exists within the five-year search window. This causes three failures:

1. ValidateCronString accepts the expression. Fix: call schedule.Next after Parse
   and reject zero results.
2. The scheduleNextJobs enqueuer loop spins indefinitely. Fix: add !t.IsZero()
   guard to the loop condition.
3. clearDirtyJobs misses negative-epoch entries. Fix: use math.MinInt64 as the
   lower bound instead of 0.

Fixes #23924

Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>

* fix(jobservice): address review feedback on unreachable cron fix

Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>

* fix(jobservice): address review feedback on unreachable cron fix

- Log and set lastEnqueueErr when policy is skipped as unreachable
- Add IsCronReachable check in jobservice Policy.Validate and controller
  submit validation so pre-upgrade schedules are rejected at restore
- Fix probe comment: century year gap, not weekday intersection
- Collapse cronIsReachable/IsCronReachable into one exported function
- Use consistent error message across all validation sites
- Fix clearDirtyJobs comment to describe legacy cleanup
- Improve test robustness: FailNow on timeout, policy-specific assertion
- Add Feb 29 leap day test case to pin reachable behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>

* fix(jobservice): stop the enqueuer loop when Next returns zero mid-loop

The zero-time check only covered the first schedule.Next call. A reachable
spec can still return time.Time{} later: for "0 0 0 29 2 *", Next after
2096-02-29 finds nothing because 2104-02-29 is past robfig/cron's five-year
search window, and time.Time{}.Before(horizon) keeps the loop spinning.

The loop now lives in fireTimes with the IsZero guard in its condition. The
warning for a zero first Next now says there is no occurrence within the
search window, because in 2097-2099 that also applies to valid Feb 29 specs.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* fix(jobservice): use dedicated enqueuer in test to fix data race

  The test goroutine called scheduleNextJobs on the suite-level enqueuer
  while its background loop was running, racing on lastEnqueueErr.

Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>

* style(jobservice): gofmt enqueuer_test.go

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Nikita Kodkany <nikitakodkany@gmail.com>
Signed-off-by: Vadim Bauer <vb@container-registry.com>
Co-authored-by: Nikita Kodkany <nkodkany@crusoe.ai>
Co-authored-by: Nikita Kodkany <nikitakodkany@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Vadim Bauer <vb@container-registry.com>
2026-10-15 06:42:50 +00:00
jUDASmILE
7396332d5c Improve job status hook handling (#24137)
Signed-off-by: jUDASmILE <judasmile@gmail.com>
2026-10-14 10:38:41 +00:00
jUDASmILE
79c614d026 Improve global label listing behavior (#24140)
Signed-off-by: jUDASmILE <judasmile@gmail.com>
2026-10-14 17:44:24 +08:00
stonezdj(Daojun Zhang)
96387e3024 fix(registryctl): reject empty secrets to prevent authentication bypass (#23946)
Clean up empty secrets when creating a new secret handler in registryctl
and skip empty secrets during validation to prevent authentication bypass
when JOBSERVICE_SECRET is empty/unset.

Signed-off-by: stonezdj <stonezdj@gmail.com>
Co-authored-by: Pushkar Joglekar <pushkar.joglekar@broadcom.com>
2026-10-14 04:51:14 +00:00
stonezdj(Daojun Zhang)
35dd9e4337 fix(scanner): redact scanner URL in candidate list for non-admin users (#23947)
Redact the URL field in ListScannerCandidatesOfProject response to prevent
non-admin project members from listing system-global scanner URLs, while
retaining the scanner URL when the caller has system admin permissions.

Signed-off-by: stonezdj <stonezdj@gmail.com>
2026-10-13 11:43:36 +00:00
stonezdj(Daojun Zhang)
5c885152b9 fix(scan): scope scan stop lookup to caller artifact (#23948)
In basicController.Stop, the execution query previously used artifact.Digest
without scoping the lookup to the caller's artifact. Because artifact digests
are content-addressable and can be identical across different projects/tenants,
this allowed users to stop scans belonging to other projects, introducing a
cross-tenant scan-stop (availability) vulnerability.

This commit scopes the execution query specifically to the artifact's unique
database ID (`artifact.ID`), ensuring that only the scan of the caller's specific
artifact is stopped.

Signed-off-by: stonezdj <stonezdj@gmail.com>
2026-10-13 09:41:33 +00:00
Viktor Erpylev
23bfa9a110 fix(ui): keep multi-value filters when editing a P2P preheat policy (#24056)
Filters with several comma-separated values are stored wrapped in braces
(e.g. {a,b}). When opening such a policy for edit, the brace-stripping
branch returned from the forEach callback instead of assigning the value,
so the repository, tag and label fields stayed empty.

The fix was originally proposed in #23323, which was closed
as stale without review; this re-submits it with a regression test.

Fixes #23293

Signed-off-by: Viktor Erpylev <velmoga@gmail.com>
Co-authored-by: Psykii22 <189542486+Psykii22@users.noreply.github.com>
2026-10-12 10:04:29 +00:00
Max
205159bd91 Custom project roles — frontend UI (#23970)
feat(portal): custom project roles UI

Frontend for the custom project roles feature (backend: #23804). Splits the
UI out of the combined feature branch and adapts it to the final backend API.

- Roles management: list/create/edit custom roles with a permissions panel
  (left-side-nav/roles/*, shared/components/role-permissions-panel/*), wired
  into routing (base.module, harbor-shell nav, shared.const) behind the
  system-admin guard.
- Member add/edit (member, add-member, add-group): dynamic role selector that
  lists built-in and custom roles from the role API, with a built-in badge.
- Robot account editor: fetch creator's effective project permissions from
  /users/current/permissions (scope=/project/{id}, relative) instead of the
  removed /projects/{name}/permissions/effective endpoint, to filter grantable
  robot permissions; fail closed on empty permissions.
- Roles grid: compact fixed widths for ID and built-in columns, multi-row selection,
  ascending ID sorting by default, and proper navigation label.
- Role pickers: shared getAllRoles() helper across member and group dialogs with
  loading state and validation against unlisted roles.
- Role wizard: exact role name uniqueness check, input validation, and full-width modal.
- i18n: ROLE.* keys and MEMBER.BUILT_IN synced and translated across all supported locales.
- Tests: unit test coverage for role permissions panel, roles component, add-role wizard,
  and role pagination helper.

Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>
Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
Signed-off-by: stonezdj <stonezdj@gmail.com>
Co-authored-by: Prasanth Baskar <prasanth@8gears.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 07:21:44 +00:00
stonezdj(Daojun Zhang)
2c94d4e213 bump: bump up Go version to v1.27.2 (#24119)
Update Go version from 1.26.4 to 1.27.2 across makefiles, configuration, CI workflows, and documentation.

Signed-off-by: stonezdj <stonezdj@gmail.com>
2026-10-12 06:28:24 +00:00
stonezdj(Daojun Zhang)
4d425b805f bump: bump up Go version to v1.26.9 (#24120)
Update Go version from 1.26.4 to 1.26.9 across makefiles, configuration, CI workflows, and documentation.

Signed-off-by: stonezdj <stonezdj@gmail.com>
2026-10-09 16:42:09 +08:00
Lewis Bertin
44d081c195 fix(i18n): translate remaining French UI strings (#24088)
Translate the strings of the French portal file that were still in
English, mostly from recent features (audit log event types, Dragonfly
preheat scope, OIDC logout, preferences).

Signed-off-by: Lewis Bertin <lewis@tactiqual.tech>
2026-10-08 10:39:19 +00:00
Wang Yan
c329590590 fix(registry): validate upload Location against the request origin, n… (#24113)
* fix(registry): validate upload Location against the request origin, not the configured endpoint

The GHSA-wjpm-hv5w-gxr6 fix compared registry-provided upload Location
headers against the configured registry endpoint (c.url). Adapters whose
authorizer rewrites the request host break under that rule: AWS ECR is
configured with https://api.ecr.<region>.amazonaws.com but every request
is rewritten to <account>.dkr.ecr.<region>.amazonaws.com, which is also
the host in the Location ECR returns. Every push replication to ECR
failed with 'resolves to a different origin'.

The correct invariant is 'the Location must be same-origin with the
server that issued it'. Validate the Location when it is received, using
the URL of the request that produced the response (after the authorizer
ran) as the origin. Root-relative Locations are still appended to the
endpoint so a path prefix is preserved. Cross-origin, network-path and
userinfo-bearing values are still rejected and the attacker host is
never contacted.

Fixes the ECR push replication regression introduced by #<GHSA-wjpm PR>.

Signed-off-by: wang yan <yan-yw.wang@broadcom.com>

* fix(registry): resolve upload Location against the final request URL after redirects

c.client.Do follows redirects, but req.URL still refers to the original
request. A same-origin 307 to another path followed by a path-relative
Location would resolve against the original path and send the next
upload to the wrong URL. Use resp.Request.URL as the origin when the
HTTP client followed redirects, but reject a redirect that changed the
origin so an arbitrary redirect target can never become a trusted,
credential-bearing upload destination.

Signed-off-by: wang yan <yan-yw.wang@broadcom.com>

* fix(registry): simplify upload Location comments

Signed-off-by: wang yan <yan-yw.wang@broadcom.com>

---------

Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
2026-10-08 08:26:47 +00:00
Prasanth Baskar
72e54002bb chore(deps): forward-port the release-2.15.0 go module bumps to main (#24093)
Three dependabot bumps landed on release-2.15.0 but never reached main:

- github.com/jackc/pgx/v5 v5.10.0 -> v5.11.0 (#23902)
- github.com/aws/aws-sdk-go-v2/credentials v1.19.19 -> v1.20.3 (#23908)
- github.com/go-openapi/runtime v0.32.2 -> v0.33.2 (#23909)

The transitive sets follow from the module graph. main is behind
release-2.15.0 on the aws-sdk core, so the aws side pulls more here than
it did on the release branch. go-openapi/runtime 0.33 moves spec,
validate, analysis and jsonpointer to 1.0.0 and brings along strfmt,
loads, the swag modules, otel and testify.

Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
2026-10-08 06:51:12 +00:00
Wang Yan
ba53f0bb10 fix(contenttrust): allow legacy signer pull by default to avoid regre… (#24085)
fix(contenttrust): allow legacy signer pull by default to avoid regression

Default CONTENT_TRUST_LEGACY_SIGNER_PULL_ENABLED to true to prevent breaking
existing cosign and notation signing workflows on upgrade.

Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
2026-10-08 04:34:08 +00:00
Vadim Bauer
f6f5bac8d1 feat(webhook): add webhook egress policy levels and allowlist (#24079)
The webhook SSRF guard was a single boolean. With it off (the default
since #24078) webhooks can reach loopback and cloud metadata; with it on,
every internal receiver breaks.

HARBOR_WEBHOOK_EGRESS_POLICY (harbor.yml webhook_egress.policy) selects a
level: allow_all, block_restricted (default) or public_only. Restricted
covers loopback, link-local, cloud metadata, unspecified, multicast and
NAT64/6to4/Teredo. HARBOR_WEBHOOK_EGRESS_ALLOWLIST takes IPs, CIDRs,
hostnames and *.domain suffixes that are allowed at every level.

When the policy is unset, the deprecated HARBOR_ALLOW_PRIVATE_NETWORK_ACCESS
keeps its 2.15 meaning: true is allow_all, false is public_only. Invalid
values fail closed, prepare rejects an unknown policy, and a blocked
target is logged with the setting that reopens it.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-08 07:15:15 +05:30
Wang Yan
9a37bcf55f fix(webhook): allow private network access by default to avoid regres… (#24078)
* fix(webhook): allow private network access by default to avoid regression

Default HARBOR_ALLOW_PRIVATE_NETWORK_ACCESS to true to prevent breaking
existing on-premise and Kubernetes deployments where webhooks target
internal services.

Remove the allow_private_network_access field from harbor.yml to keep
the YAML schema clean, managing this toggle solely via the environment
variable.

Signed-off-by: wang yan <yan-yw.wang@broadcom.com>

* resolve copilot review

Signed-off-by: wang yan <yan-yw.wang@broadcom.com>

* resolve the comments

Signed-off-by: wang yan <yan-yw.wang@broadcom.com>

---------

Signed-off-by: wang yan <yan-yw.wang@broadcom.com>
2026-10-07 20:04:17 +08:00
Vadim Bauer
b323c0c8ec Merge commit from fork
* fix(webhook): block SSRF to private-network targets and stop reflecting response bodies

Webhook and Slack notification jobs delivered to any operator-supplied URL with
no address classification, and folded a non-2xx response body into the
project-readable task log. A project admin could reach loopback, RFC1918,
link-local and cloud-metadata (169.254.169.254) targets and read internal
service responses back through the webhook task log (CWE-918).

Classify destinations against the IANA special-purpose registries and reject
non-public targets at (1) webhook policy validation and (2) a dial-time guard on
the notification HTTP clients; drop the response body from the task-log error;
refuse redirects. The HARBOR_ALLOW_PRIVATE_NETWORK_ACCESS opt-out (default off)
restores delivery to internal endpoints for operators who rely on it.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(webhook): cover SSRF target rejection, body-drop and redirect refusal

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(webhook): resolve upstream's mixed-case target host in the stub resolver

Upstream #23889 added a create-policy test that posts a mixed-case target
address and expects 201. With the SSRF target check in place that hostname
now goes through the stub resolver, which only knew hooks.example.com, so
the test failed with 400. Teach the stub the second hostname; the upstream
test keeps asserting lowercasing, the SSRF tests keep asserting rejection.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* fix(webhook): honour the jobservice proxy settings in the SSRF-safe transport

The public-network guard cleared Transport.Proxy, so webhook and Slack
delivery ignored the HTTP_PROXY/HTTPS_PROXY/NO_PROXY settings that
prepare writes for jobservice. Deployments without a direct Internet
route lost every public webhook.

Keep the transport's proxy function. When it selects a proxy, validate
the target host (literal or all resolved addresses) as public before the
request is handed to the proxy, and let only dials to that proxy address
skip the dial-time private-address check. When no proxy applies
(NO_PROXY, loopback), the resolved-address dial guard is unchanged. A
direct request aimed at the proxy's own address is refused so it cannot
inherit the proxy exemption.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* chore: describe the fix without advisory identifiers

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 54cc3fca7a741ecb31293a767b998ceffec9d5b5)

* fix(webhook): drain notification response bodies before closing

Closing an unread response body forces the transport to drop the
keep-alive connection. Discard up to 64KiB of the body (never surfacing
it) so the pooled connection is reused after non-2xx responses.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(webhook): assert the dial guard on production clients and pin the env

Exercise the production notification clients against a loopback target
so dropping the public-network guard fails a test, cover connection
reuse after non-2xx responses, and pin HARBOR_ALLOW_PRIVATE_NETWORK_ACCESS
to false in tests that assert the default blocking behaviour.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* fix(prepare): keep network.allow_private_network_access across migrate

The migrate command re-renders harbor.yml from the 2.15.0 template, so a
key missing there is dropped. Render the network block, preserving an
operator-set value and defaulting to false.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 14:04:35 +02:00
Vadim Bauer
f234c0cb68 Merge commit from fork
* fix(policy): stop User-Agent from bypassing content-trust and vulnerability policies

SkipPolicyChecking exempted a caller from the content-trust and
vulnerability-prevention pull policies when it held push and the request
User-Agent contained "cosign" or "notation". User-Agent is fully
attacker-controlled (CWE-807), so any push-capable principal (e.g. the
Developer role) bypassed both policies on a plain manifest pull by setting
one header.

Remove the User-Agent discriminator. Scanner pull keeps its bypass via the
non-forgeable scanner-pull RBAC action, and a genuinely signed artifact is
still recognised by its signature accessory. The pull-before-first-signature
bootstrap for content-trust is preserved as an opt-in, content-trust-only
exemption (CONTENT_TRUST_LEGACY_SIGNER_PULL_ENABLED=true); it never affects
the vulnerability-prevention policy.

Reported by sabuhimammadov0 (GHSA-wrw5-gmvj-gf23); independently reported on
the cncf-harbor-security list by kt (thread #61), Oleh Konko (thread #25) and
Eren Akdag (thread #68).

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(policy): spoofed User-Agent no longer bypasses pull policies

Cover GHSA-wrw5-gmvj-gf23: a push-capable v2token caller with
User-Agent: cosign is now blocked by both the content-trust and the
vulnerability-prevention middleware, while scanner-pull and genuine signature
accessories still bypass. Add a test for the opt-in content-trust bootstrap.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(contenttrust): pin the legacy signer-pull hatch off in the spoofed User-Agent test

The test asserted the hatch-off behaviour but inherited the process
environment, so it failed whenever the flag was exported.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(vulnerable): enable the legacy signer-pull hatch in the spoofed User-Agent test

With the flag off the test could not detect the User-Agent bypass
being moved back into the shared policy check behind the flag.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* chore: describe the fix without advisory identifiers

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 49f78850621446dd113a3d2517fed7e23f3e565b)

* test(contenttrust): cover the legacy signer-pull hatch guards

Add negative controls for the opt-in signer-pull exemption: it stays off
when the variable is unset, and with it enabled a pull is still rejected
without push permission or without a signing-client User-Agent. Also
cover the notation User-Agent path.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 14:03:35 +02:00
Vadim Bauer
0a9e68e6d5 Merge commit from fork
* test(pkg/registry): regression for chunk-upload Location SSRF (GHSA-wjpm-hv5w-gxr6)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* fix(registry): validate chunk-upload Location against the registry origin (GHSA-wjpm-hv5w-gxr6)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* fix(registry): keep the endpoint path prefix for root-relative upload Locations

Distribution with relativeurls returns /v2/... without the path prefix the endpoint is served under. Resolving it against the endpoint dropped that prefix, so pushes through a path-prefixed registry endpoint hit the wrong route. Append root-relative paths to the endpoint as before and keep the origin and userinfo checks.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* chore: describe the fix without advisory identifiers

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 6b4be4734a401c5b427667544534aea8bed53c1d)

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 14:02:35 +02:00
Vadim Bauer
c38a9eb5d8 Merge commit from fork
* test(common/http): regression for pagination Link SSRF (GHSA-xgp2-5vxg-rgh2)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* fix(common/http): pin pagination Link next URL to the request origin (GHSA-xgp2-5vxg-rgh2)

GetAndIteratePagination built the next-page URL by string-concatenating the
attacker-controlled Link rel="next" value onto scheme+host and re-parsing it, so
a value like "@evil.example/x" re-pointed the request at evil.example (host) with
the original host demoted to userinfo. Because Client.Do re-runs its auth modifiers
on every hop, the configured registry credentials were then sent to that host
(SSRF + credential disclosure).

Resolve the Link as an RFC 3986 reference against the current page URL, reject
userinfo, and require the resolved scheme/host/port to equal the origin. Relative
same-origin pagination keeps working; any cross-origin next target fails closed.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(common/http): require cross-origin pagination links to fail with an error

The regression test discarded the pagination error, so an implementation that silently stopped at a cross-origin Link and returned a truncated list would still pass. Assert that every vector returns an error.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit bfa9a1e750cceb9363a510d548f14694fdf6ae31)

* chore: describe the fix without advisory identifiers

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 00e1558ff2fd848ebd071d21a9a9b3e1895df761)

* test(common/http): cover userinfo rejection and absolute same-origin pagination links

The existing userinfo vector is rejected by url.Parse before the
userinfo check runs, so add resolveNextLink cases that only that check
can reject. Also exercise absolute same-origin next links, default port
equivalence and scheme/port mismatches.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* fix(registry): keep catalog and tag list pagination on the registry origin

Catalog and ListTags followed the Link rel="next" value from the remote
registry verbatim when absolute, and appended it to the endpoint otherwise,
so an absolute URL to another host or a value starting with "@" sent the
next request elsewhere. Authorizers that attach credentials regardless of
host (basic auth used by the JFrog adapter, the ACR token authorizer) then
disclosed them to that host.

Build the next URL exactly as before and reject it unless it keeps the
endpoint's scheme, host, effective port and userinfo. SameOrigin is
exported from common/http to share the origin check.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 14:01:35 +02:00
Vadim Bauer
8d59dd5bba Merge commit from fork
* fix(security): fail closed when auth mode lookup fails (GHSA-3x7c-wf86-mjx6)

The security context middleware called config.AuthMode and, on error, logged a
warning and continued processing the request with no auth mode in the request
context. Determining the request's security context is an authorization decision
that depends on the configured auth mode, so a lookup failure must reject the
request rather than fall through to the handler (CWE-636).

Reject with 500 via lib_http.SendError before the security-context generators
run. Normal requests, where the lookup succeeds, are unaffected.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(security): assert middleware fails closed on auth mode lookup error (GHSA-3x7c-wf86-mjx6)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* chore: describe the fix without advisory identifiers

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 33304265ce4ad826c63e471fd65bcbf2aa589c86)

* test(security): restore default config manager after TestSecurity

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 14:00:35 +02:00
Vadim Bauer
d80de5b309 Merge commit from fork
* fix(authproxy): stop a local sysadmin flag leaking into an authproxy context (GHSA-xvh7-74g6-29xw)

In auth_mode=http_auth the /v2 path loads the Harbor-local user record by name
and overlays only GroupIDs and AdminRoleInAuth from the TokenReview result
(server/middleware/security/auth_proxy.go). The local SysAdminFlag survives, and
local.SecurityContext.IsSysAdmin returns SysAdminFlag || AdminRoleInAuth, so an
external identity that the authproxy admin policy did not grant admin obtains
sysadmin /v2 authorization purely by colliding with a local sysadmin name -- the
seeded admin account always exists.

Clear SysAdminFlag on the overlaid record so AdminRoleInAuth, derived from the
TokenReview username and groups against the configured admin set, is the only
admin signal in authproxy mode. An explicitly allowlisted authproxy admin is
unaffected: its access comes from AdminRoleInAuth, not a local flag.

Reported by cyberlanc3r (GHSA-xvh7-74g6-29xw); independently reported by
Mitchell Benjamin (thread #49, topic 119514428).

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(authproxy): regression for sysadmin-flag collision (GHSA-xvh7-74g6-29xw)

Drive the real authProxy.Generate + local.SecurityContext.IsSysAdmin with a
mocked user manager returning a colliding sysadmin-flagged local record. Assert
the external identity is not sysadmin with an empty admin allowlist, and that an
explicitly allowlisted authproxy admin still is.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* chore: describe the fix without advisory identifiers

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit df6273ae8d0dd15a0a1f2f84d5a9b3f5b018bdee)

* test(authproxy): return a fresh user per lookup in the collision test

The mocked GetByName returned one shared *User, and Generate mutates it,
so the allowlisted arm saw a record whose sysadmin flag the first arm had
already cleared and never exercised the collision. Return a new record
per call, as the DB-backed manager does, and assert the allowlisted arm
derives admin from AdminRoleInAuth with the local flag cleared.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 13:59:35 +02:00
Vadim Bauer
d8a18f71b4 Merge commit from fork
* fix(immutable): evaluate all repository and tag selectors (GHSA-27f2-qg57-2gwg)

The immutable tag rule matcher only read ScopeSelectors["repository"][0] and
TagSelectors[0], so a rule carrying more than one repository or tag selector
protected only the first entry. Pushes and deletes to the repositories or tags
named by the remaining selectors bypassed the immutability control.

Evaluate every selector in each dimension and match when any of them matches,
mirroring the OR/alternation the portal expresses as a single {a,b} doublestar
pattern. Single-selector rules, which is all the portal ever creates, keep
identical behaviour.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(immutable): cover multi-selector immutable rule matching (GHSA-27f2-qg57-2gwg)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* fix(immutable): subtract exclusion selectors instead of OR-ing them

Several selectors in one rule dimension must behave like the portal's
single {a,b} pattern. OR-ing every selector made two repoExcludes or
excludes selectors select everything, so excluded repositories and
tags became immutable. Inclusion selectors stay alternatives, every
exclusion selector now has to select the candidate, and multi-tag
candidates are evaluated per tag so tags excluded by different
selectors do not cancel out.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* chore: describe the fix without advisory identifiers

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 27f4614d1ec7484eda1336bb2e01cf1b891326df)

* fix(immutable): ignore null selectors in rule matching

The rule API stores selector lists without validation, so a null entry
after the first selector was harmless while only the first one was read.
Evaluating every selector turned such entries into a nil dereference.
Drop null entries before matching.

Tests now purge leftover rules of their project before running, since
Match evaluates every rule of the project.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 13:58:35 +02:00
Vadim Bauer
7894b309c3 Merge commit from fork
* fix(scan): verify TLS and bound body on scanner bearer token request (GHSA-884q-mjcg-86x4)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(scan): assert bearer token request verifies TLS and bounds body (GHSA-884q-mjcg-86x4)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* chore: describe the fix without advisory identifiers

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit c9d2c8dc3997ef80fb1ea8a09936d01a13bab8e9)

* test(scan): assert certificate verification error type in bearer token TLS test

Match *tls.CertificateVerificationError via errors.As instead of a
substring of the error message, so the test does not depend on x509
error wording and cannot pass on an unrelated failure.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 13:57:35 +02:00
Vadim Bauer
f5c89c7355 Merge commit from fork
* fix(scanner): stop access_cred ORM filter oracle (GHSA-vwvv-675v-p6rr)

The scanner_registration.access_cred column stores the literal Authorization
value Harbor presents to a scanner adapter. It lacked the filter:"false" ORM
tag, so the generic q= builder let a caller put it in a WHERE clause. Since the
API never returns the column, the row/X-Total-Count difference is a blind
boolean oracle that recovers the secret character by character (sibling of
CVE-2025-30086, matching the guarded reg.access_secret). Add filter:"false".

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(scanner): regression for access_cred filter oracle (GHSA-vwvv-675v-p6rr)

Drives the real ListRegistrations/GetTotalOfRegistrations DAO path (the call
ListScannerCandidatesOfProject makes) with a non-matching access_cred filter and
asserts list/count are unchanged, i.e. the filter is dropped. Red without the
filter:"false" tag (oracle excludes the row), green with it. Positive control
keeps name filtering working.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* chore: describe the fix without advisory identifiers

Signed-off-by: Vadim Bauer <vb@container-registry.com>
(cherry picked from commit 9c160302ee3c56394957b263061adf374db4502f)

* docs(scanner): describe access_cred storage accurately

New writes are encrypted by the scanner manager; rows saved before that
remain plaintext because there is no data migration.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 13:56:35 +02:00
Vadim Bauer
1ab096098a Merge commit from fork
* fix(rbac): collapse cover-all robot project scope to a single-segment matcher (GHSA-xq2m-cj8w-56v5)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(rbac): regression for cover-all robot sub-resource escalation (GHSA-xq2m-cj8w-56v5)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* chore(rbac): make the cover-all robot scope comment generic

Review follow-up: the comment and the test docs now describe the wildcard
expansion and the single-segment matcher in general terms instead of naming
the advisory. Wording only, no behaviour change.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 13:55:35 +02:00
Vadim Bauer
5d0c95721e Merge commit from fork
## Summary

This PR contributes the focused fix for Mitchell Benjamin's private thread #45 report. Granular
user-update authority no longer permits a caller to mint a Harbor sysadmin.

## Patch shape

An explicit `IsSysAdmin` guard follows the existing system permission check and precedes the sole
production `SetSysAdmin` call. Other user automation permissions are unchanged.

## Verification

The routed robot-denial and true-sysadmin success arms were checked and formatted against the pin.
Generated APIs were unavailable, so compile, focused handler test, and a served `403`/`200` check
remain reviewer tasks.

PTP-Ref: PTP-HARBOR-B-45
Class: B
Upstream-Report: thread #45, topic 119493101, cncf-harbor-security list
Original-Reporter: Mitchell Benjamin
Issue: TBD
Patch: patches/BACKLOG-45.patch
Base: 934ba4c60d
Audit-Pin: 1cd644ca39
Evidence: poc-out/45/ (receipt), patches/anish/BACKLOG-45.md (rationale)

Ported-From: https://github.com/container-registry/harbor-vuln-fixes/pull/8

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Co-authored-by: Clinton Thomas <1033162+KernelClint@users.noreply.github.com>
2026-10-06 13:54:30 +02:00
Vadim Bauer
d57831f1df Merge commit from fork
* fix(registry): reject dot segments in /v2/ distribution paths (GHSA-qj39-pjf2-wmrc)

The /v2/ authorization middleware matches the raw request path to decide
which repository is accessed, while the beego router and the backend
registry resolve the handler after path.Clean. A path such as
/v2/public/x/manifests/x/../../../../private/secret/manifests/1.0.0 is
authorized as public/x but dispatched to private/secret, so anonymous or
low-privileged callers can read, overwrite or delete artifacts in projects
they cannot access.

Reject any /v2/ request whose URL path contains a '.' or '..' segment
before the auth middleware runs. No legitimate OCI path contains a dot
segment, so this fails closed with no impact on valid clients.

Signed-off-by: Vadim Bauer <vb@container-registry.com>

* test(registry): regression for /v2/ dot-segment path traversal (GHSA-qj39-pjf2-wmrc)

Signed-off-by: Vadim Bauer <vb@container-registry.com>

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-06 13:53:09 +02:00
dependabot[bot]
721c6d48d9 chore(deps-dev): bump stylelint from 14.16.1 to 17.15.0 in /src/portal (#23957)
* chore(deps-dev): bump stylelint from 14.16.1 to 17.15.0 in /src/portal

Bumps [stylelint](https://github.com/stylelint/stylelint) from 14.16.1 to 17.15.0.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/14.16.1...17.15.0)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-version: 17.15.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(portal): drop the deprecated stylelint prettier configs

stylelint-config-prettier caps its stylelint peer at < 15, so npm ci
fails with ERESOLVE once stylelint is on 17. Both prettier configs only
existed to switch off stylelint's formatting rules, which stylelint
removed in 16, and both packages are deprecated. Remove them from
package.json and .stylelintrc.json.

Bump stylelint-config-standard to 40.0.0 and
stylelint-config-standard-scss to 17.0.0, the first versions that accept
stylelint 17.

The newer configs flag 30 lines. 26 are fixed with stylelint --fix and
render the same (longhands to inset/place-* shorthands, word-wrap to
overflow-wrap, quoted attribute selectors, spacing). The remaining four
use deprecated CSS that browsers still support (clip: rect() in the
gauge, word-break: break-word in the alert banner). Replacing them could
change how they render, so they get inline disable comments instead.

Signed-off-by: Prasanth Baskar <prasanth@8gears.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Prasanth Baskar <prasanth@8gears.com>
2026-10-05 00:39:25 +00:00
dependabot[bot]
1f54a55a2b chore(deps): bump marked from 18.0.12 to 18.0.13 in /src/portal in the patch-updates group across 1 directory (#23952)
chore(deps): bump marked

Bumps the patch-updates group with 1 update in the /src/portal directory: [marked](https://github.com/markedjs/marked).


Updates `marked` from 18.0.12 to 18.0.13
- [Release notes](https://github.com/markedjs/marked/releases)
- [Commits](https://github.com/markedjs/marked/compare/v18.0.12...v18.0.13)

---
updated-dependencies:
- dependency-name: marked
  dependency-version: 18.0.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vadim Bauer <vb@container-registry.com>
Co-authored-by: Prasanth Baskar <prasanth@8gears.com>
2026-10-04 21:54:46 +00:00
dependabot[bot]
3f56f04403 chore(deps-dev): bump https-proxy-agent from 5.0.1 to 9.1.0 in /src/portal (#23954)
chore(deps-dev): bump https-proxy-agent in /src/portal

Bumps [https-proxy-agent](https://github.com/TooTallNate/proxy-agents/tree/HEAD/packages/https-proxy-agent) from 5.0.1 to 9.1.0.
- [Release notes](https://github.com/TooTallNate/proxy-agents/releases)
- [Changelog](https://github.com/TooTallNate/proxy-agents/blob/main/packages/https-proxy-agent/CHANGELOG.md)
- [Commits](https://github.com/TooTallNate/proxy-agents/commits/https-proxy-agent@9.1.0/packages/https-proxy-agent)

---
updated-dependencies:
- dependency-name: https-proxy-agent
  dependency-version: 9.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vadim Bauer <vb@container-registry.com>
Co-authored-by: Prasanth Baskar <prasanth@8gears.com>
2026-10-04 19:49:17 +00:00
Vadim Bauer
f25e9dafbf fix(gc): Use the current registry Redis URL for scheduled runs (#24037)
Scheduled runs now take the URL from core's current _REDIS_URL_REG and
only fall back to the stored value when the variable is empty. Existing
schedules recover without being saved again.

Fixes #22905

Signed-off-by: Vadim Bauer <vb@container-registry.com>
2026-10-01 11:44:38 +05:30
Max
37dc02fdff feat: custom project roles (hybrid — built-ins in code, custom in DB) (#23804)
* feat: custom project roles (hybrid — built-ins in code, custom in DB)

Adds admin-defined custom project roles as an alternative to #22815, per
maintainer feedback: built-in project roles keep resolving their permissions
from the compile-time rolePoliciesMap (no DB, no cache — identical to today),
and ONLY custom roles are stored in and loaded from the database. There is no
permission cache.

Backend + API only (UI ships separately). Highlights:
- RBAC (common/rbac/project): projectRBACRole gains an optional custom *role.Role.
  The built-in path is byte-identical to upstream; the role controller is
  consulted only for non-built-in role IDs, so built-in authorization incurs no
  DB lookup. Custom roles expand their DB-loaded permissions.
- Role subsystem: pkg/role (model/dao/manager) + controller/role for CRUD and
  persistence; existing role_permission/permission_policy reused for custom perms.
- REST API: /roles endpoints (sysadmin-gated), an effective-permissions endpoint,
  anti-escalation checks on member/robot assignment, and audit events.
- Migration 0191 adds is_builtin/description/audit columns to `role`, widens the
  name, enforces name uniqueness, and marks seeded roles built-in. Built-in
  permissions are NOT seeded into the DB.

Rebased onto current main; API bindings regenerated via gen_apis.

Tracking: #18124
Alternative to: #22815

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: make role a system RBAC resource; drop checkSysAdmin & ResourceSelf

Addresses stonezdj's review on PR #23804:
- Define rbac.ResourceRole (system-level) and add it to the system permission
  catalog; gate CreateRole/UpdateRole/DeleteRole with RequireSystemAccess(action,
  ResourceRole) instead of the hand-rolled checkSysAdmin — so the permission is
  managable while still effectively system-only.
- Remove the checkSysAdmin helper and its unit tests; update handler tests to
  mock Can().
- Drop ResourceSelf from the ScopeRole permission catalog.
- Remove tests/integration/.gitignore (per-dir ignore file).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: close built-in-role escalation gap; reject nil role payloads

Addresses Copilot findings on PR #23804:
- checkNoEscalation treated built-in roles as permissionless: it read permissions
  from role_permission, which has no rows for built-in roles, so a caller with
  member:create could assign projectAdmin (or any built-in) without holding its
  permissions. Built-in permissions are now synthesized from the compile-time
  policy map via the new rbac/project.BuiltinRolePolicies; custom roles still read
  the DB. Adds TestCheckNoEscalation_BuiltinRoleSynthesized.
- role validate() now rejects nil permission/access elements (permissions:[null],
  access:[null]) with 400 instead of panicking. Adds nil-payload tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: validate that a member's custom role exists in the database

Addresses stonezdj's review on PR #23804: isValidRole returned true for any
integer above the built-in range, so a member could be assigned a non-existent
custom role ID. The member controller now holds a role manager and validateRole
verifies custom role IDs (>5) exist via roleMgr.Get, rejecting unknown IDs with
ErrInvalidRole; built-in roles (1-5) still pass without a DB lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: make role Create/Update/Delete atomic via orm.WithTransaction

Addresses Copilot findings on PR #23804:
- Create: the role row and its permission records are now created in one
  transaction, so a failed permission insert no longer leaves a named role with
  missing permissions (whose unique name would block a clean retry).
- Update: the field update and the delete+recreate of permissions run in one
  transaction, so the role's authorization is never observable in a partial state.
- Delete: the member-assignment guard and the role/permission deletes run in one
  transaction, serializing the guard with the delete and ensuring permission rows
  never outlive the role. (Full prevention of a concurrent assignment needs a
  DB-level FK on project_member.role — tracked as a follow-up.)

Controller unit tests inject a fake ormer (testing/lib/orm) so the transaction
wrappers resolve an ormer from context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: group /roles/{role_id} right after /roles in swagger

Addresses stonezdj's review on PR #23804: the two role endpoints serve the same
feature and should be adjacent in the spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: split out role audit-log events; drop the standalone integration test

Addresses stonezdj's review on PR #23804 (shrink the PR):
- Remove the role audit-log event wiring from this PR (controller/event
  metadata/role.go, topic.go role topics/events, init.go subscriptions, and the
  notification.AddEvent calls in the role controller). It will be submitted as a
  separate follow-up PR; the code is preserved on branch 18124-role-audit-events.
- Remove tests/integration/test_custom_roles.py: it is not wired into Harbor CI
  (which uses tests/apitests) and contained the issues Copilot flagged. Feature
  behavior is covered by the Go unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: validate role on member UpdateRole; fix stale invalid-role message

Addresses stonezdj's follow-up review on PR #23804:
- UpdateRole (changing a member's role) now runs the same validateRole check as
  AddProjectMember, so a member can't be moved to a non-existent role.
- ErrInvalidRole no longer claims "role is not in 1,2,3"; it now states the role
  must be a built-in role (1-5) or an existing custom role.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: remove duplicate effective-permissions endpoint

Addresses stonezdj's review on PR #23804: GET /projects/{id}/permissions/effective
duplicated the existing GET /users/current/permissions, which already supports a
project scope and a `relative` flag (scope=/project/{id}&relative=true returns the
same set of project permissions the caller holds). Removed the handler and the
swagger path (bindings regenerated). The shared mapRobotToHumanResource helper is
kept — it is still used by the robot anti-escalation check.

The robot-account UI (separate PR) will use /users/current/permissions instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* fix(rbac): keep role management sysadmin-only, not delegable

Role write access is checked via RequireSystemAccess(ctx, action,
rbac.ResourceRole), but ResourceRole was also added to the grantable
ScopeSystem catalog (NolimitProvider). That published role:* as an
assignable system permission: it could be granted to a system robot
(robots evaluate system-scope policies) and listed in /permissions,
even though a human non-admin never evaluates system-scope perms.

A system robot granted role:update could then rewrite any custom
role's permission set (UpdateRole/updateV2Role has no subset guard)
= privilege escalation.

Remove the ResourceRole entries from the ScopeSystem catalog. The
admin evaluator bypasses the catalog, so sysadmins still pass the
RequireSystemAccess check and manage roles exactly as before, but
role:* is no longer assignable/enumerable to non-admins. This keeps
role management strictly sysadmin-only while still expressing it as
a first-class action+resource check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* fix(rbac): grant custom-role members baseline self:read (project visibility)

self:read (ResourceSelf) is the permission that gates viewing a project:
RequireProjectAccess(ctx, id, ActionRead) with no subresource checks
<project-ns>:"" and GetProject/GetSummary use exactly that. Built-in
roles carry self:read in rolePoliciesMap, and public projects grant it
via publicProjectPolicies, but a custom role in a private project had no
way to obtain it -- self:* is intentionally excluded from the ScopeRole
catalog, and GetPolicies returned only the role's explicit permissions.

Result: a member whose only access to a private project was a custom
role got a 403 opening that project.

Append {ResourceSelf, ActionRead} to the custom-role branch of
projectRBACRole.GetPolicies (de-duplicated), mirroring the built-in
roles and the public-project baseline. Only self:read is granted, never
self:update/self:delete (project edit/delete stay exclusive to the
projectAdmin built-in), and self:* remains out of the user-facing
catalog so it is guaranteed by the system rather than a tickable option.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* refactor(rbac): grant self:read at membership level, not per custom role

Move the baseline project-visibility grant out of the custom-role branch
of projectRBACRole.GetPolicies and into rbacUser.GetPolicies: any user
who holds at least one role in a project (len(projectRoles) > 0) is
granted self:read on that project.

This makes baseline visibility a property of project membership rather
than a per-custom-role special case, sitting beside the existing
public-project baseline (getPoliciesForPublicProject) and using the same
eval-time idiom -- no permission data seeded into the database. Built-in
and custom roles now obtain self:read the same way, and the grant stays
scoped to the membership project (ListRoles is per-project), so it never
exposes projects the user is not a member of.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* chore(deps-dev): bump webpack-cli from 4.10.0 to 7.2.3 in /src/portal/app-swagger-ui (#23850)

chore(deps-dev): bump webpack-cli in /src/portal/app-swagger-ui

Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.10.0 to 7.2.3.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.10.0...webpack-cli@7.2.3)

---
updated-dependencies:
- dependency-name: webpack-cli
  dependency-version: 7.2.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Prasanth Baskar <prasanth@8gears.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: consolidate 2.16.0 migration into 0190 and simplify robot resource map

Address two review comments from stonezdj on PR #23804:

- 2.16.0 is not released yet, so keep all its schema changes in a single
  migration file. Move the custom-role `role` table additions (columns,
  name widened to varchar(255), uq_role_name unique index, is_builtin
  update) from 0191 into 0190 and delete 0191.

- Simplify mapRobotToHumanResource: replace the single-entry
  robotToHumanResourceMap + lookup with a direct
  `if r == rbac.ResourceProject { return rbac.ResourceSelf }`.

Also commit the cross-project isolation test for the membership-level
self:read baseline (a member of one private project cannot read another).

No behavior change; robot resource-mapping tests pass unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: fix bugs from bupd's review (keep roles global + sysadmin-only)

Address the correctness findings from bupd's review of PR #23804, keeping
the global, sysadmin-managed role model (project-scoped roles remain a
separate future feature):

- evaluator: a failed custom-role load no longer drops the whole rbacUser.
  Returning nil stripped every role the member held in the project (built-ins
  included), turning a transient DB error or a dangling project_member.role
  into a total 403 / permanent lockout. Skip only the failing role instead.
- role validate(): reject any permission Effect other than "" or "allow".
  It was stored verbatim and handed to casbin, so a mis-cased "Allow" or a
  bogus value was accepted and silently granted nothing.
- read path: GET /roles(/{id}) now returns a built-in role's real permissions
  (resolved from rolePoliciesMap via BuiltinRolePolicies) instead of an empty
  set; built-ins have no role_permission rows by design. Removed the dead
  "Visible" list filter (model.Role has no such field).
- controller Delete: a custom role with no permission rows is deletable again
  (DeletePermissionsByRole's NotFound no longer rolls back the txn); dropped
  the unused variadic option parameter.
- controller Update: persist a role name change instead of silently dropping
  it (validated in the handler; DB unique index rejects collisions).
- permissions.go: restore the sysadmin-or-project-admin gate on GET
  /permissions (it had been relaxed to any authenticated user), and expose the
  role-level catalog only to sysadmins.
- rolePoliciesMap["projectAdmin"]: add metadata:list, restoring 2.15 behavior
  where a project admin can create a robot with metadata:list (the new robot
  anti-escalation check validated against projectAdmin, which lacked it).
- swagger: document that role reads are open to any authenticated user while
  writes are sysadmin-only.
- add a drift-guard test asserting the ScopeRole catalog equals projectAdmin
  minus self:{read,update,delete}, plus tests for the fixes above.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: make role name uniqueness case-insensitive

Per bupd's review: enforce uniqueness on lower(name) so "Maintainer" and
"maintainer" cannot coexist and be confused for one another. 2.16.0 is
unreleased, so the index is dropped and recreated in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* fix(role): correct stale TestList assertion (roles have no "visible" concept)

TestDaoTestSuite/TestList asserted that a freshly created role is NOT
returned by List (len == 0), a leftover from a removed "visible" column.
model.Role has no such field and the List DAO applies no visibility
filter, so the role is returned. Assert it is listed (len == 1) to match
the actual, intended behavior. Surfaced now that UTTEST runs on the PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

* review: robot-refresh escalation, deny-effect guard, delete-race FK, swagger 409/412

Address chlins' and Copilot's review of PR #23804:

- RefreshSec escalation (chlins #1): refreshing a robot's secret is
  equivalent to being granted the robot's permissions, so it now runs the
  same no-escalation check as create/update (load the robot with
  permissions; human callers checked via validateNoEscalation, robot
  callers via isValidPermissionScope). A member with robot:update but not
  the robot's own permissions can no longer take over a more-privileged
  robot.
- Deny-effect guard (chlins #2): member checkNoEscalation now skips
  Effect==deny accesses; they restrict rather than grant, so requiring the
  caller to hold them wrongly blocked assigning a role that merely denies
  something.
- Delete-vs-assign race (chlins #3): add FK project_member.role ->
  role(role_id) ON DELETE RESTRICT so the DB atomically rejects deleting an
  assigned role, closing the count-then-delete race; the resulting FK error
  is mapped to 412 in the role Delete controller.
- Swagger: document 409 (CreateRole duplicate-name conflict) and 412
  (DeleteRole still-assigned) responses.
- Tests: FK-race delete -> 412, and a Create failure-injection so dropping
  the transaction would fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>

---------

Signed-off-by: Max Grau Stenzel <max.grau.stenzel@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Prasanth Baskar <prasanth@8gears.com>
2026-09-29 19:51:23 +08:00