Skip to content

feat: restrict who is shown a buildAddUrl link, and enforce it on the redirect - #44

Merged
kurok merged 1 commit into
masterfrom
feat/buildaddurl-visibility
Sep 18, 2026
Merged

kurok merged 1 commit into
masterfrom
feat/buildaddurl-visibility

Conversation

@kurok

@kurok kurok commented Sep 18, 2026

Copy link
Copy Markdown

Closes #41.

buildAddUrl(title: 'Deploy to PROD', url: '/job/deploy-prod/parambuild/?v=1.2.3',
            visibleTo: [users: ['alice'], groups: ['release-managers']])

Optional — listing nothing means no narrowing, and existing behaviour is unchanged.

Why not upstream's version

It returned null from getIconFileName() and stopped. That hides an icon, which is not access control: the target URL is in the page source, and on this fork the link is a Stapler-routed redirect whose path is derivable from the title and url.

So the check runs in all three places a link surfaces — the sidebar icon, the details-bar Detail, and doIndex, which now answers 404 instead of redirecting someone who was never meant to see it. The lgtm[jenkins/no-permission-check] justification on doIndex is gone, because it no longer applies.

Upstream also hand-rolled membership against User.getAuthorities() while ignoring the authorization strategy — which hid the link from an administrator who was not on the list. This only narrows what Jenkins has already granted; it cannot widen anything.

The tests fail without the enforcement, one per surface

aRestrictedLinkIsHiddenAndRefusedForEveryoneElse
  no sidebar icon ==> expected: <null> but was: </plugin/deploy-dashboard/deploy.png>
aGroupGrantsVisibility
  expected: <404> but was: <302>

That 302 is upstream's design failing exactly as described — hidden in the UI, served on request.

A bug the round-trip test caught

The step had no config.jelly at all, so the freestyle form and the Snippet Generator both rendered an empty panel. Adding one exposed this:

theFreestyleFormRoundTripsVisibleTo
  expected: <[alice]> but was: <[[alice]]>

A textarea submits one string, and binding that onto a List<String> stores the list's own toString() as a single element. The form rendered perfectly while quietly corrupting what was typed into it. It binds through text properties now. I would not have found this without writing the round trip, and a form that discards input is worse than no form.

Scope, stated in the README

visibleTo is not the security boundary for deploying. The job the link points at enforces its own permissions — a parambuild URL still requires Item.BUILD there. This decides who is shown a shortcut, and the README says so where someone configuring it will read it.

Local mvn clean verify: 44 tests, green.

… redirect

    buildAddUrl(title: 'Deploy to PROD', url: '/job/deploy-prod/parambuild/?v=1.2.3',
                visibleTo: [users: ['alice'], groups: ['release-managers']])

Optional. Listing nothing means no narrowing, and behaviour is unchanged.

The upstream proposal this comes from returned null out of getIconFileName and
stopped there. That hides an icon, which is not access control: the target URL
is in the page source, and this fork serves the link as a Stapler-routed
redirect whose path is derivable from the title and url. So the check runs in
all three places a link surfaces -- the classic sidebar icon, the details-bar
Detail, and doIndex, which now answers 404 rather than redirecting someone who
was not meant to see it. The lgtm[jenkins/no-permission-check] justification on
doIndex is gone because it no longer applies.

Upstream also hand-rolled membership against User.getAuthorities() and ignored
the authorization strategy, which hid the link from an administrator who was
not on the list. This only narrows what Jenkins has already granted: it cannot
widen anything, and an unset visibleTo is everyone who can read the build.

Two tests fail without the enforcement, one per surface, the second being the
one that matters:

    aRestrictedLinkIsHiddenAndRefusedForEveryoneElse
      no sidebar icon ==> expected: <null> but was: </plugin/.../deploy.png>
    aGroupGrantsVisibility
      expected: <404> but was: <302>

That 302 is upstream's design failing exactly as described: hidden in the UI,
served on request.

The step also gains a config.jelly, since it had none and both the freestyle
form and the Snippet Generator rendered an empty panel. The form binds through
text properties rather than the lists: a textarea submits one string, and
binding that onto a List<String> stores the list's own toString as a single
element, so a round trip of ["alice"] came back as ["[alice]"]. It rendered
perfectly while quietly corrupting what was typed, which only the round-trip
test showed.

README documents visibleTo, and says plainly that it is not the security
boundary for deploying: the target job still enforces Item.BUILD, and this
decides who is shown a shortcut.

Signed-off-by: kurok <22548029+kurok@users.noreply.github.com>
@kurok
kurok merged commit ebf0b93 into master Sep 18, 2026
6 checks passed
@kurok
kurok deleted the feat/buildaddurl-visibility branch September 18, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restrict buildAddUrl link visibility to users/groups, enforced on the redirect too (from upstream PR jenkinsci#6)

1 participant