Re-design UI#749
Conversation
|
There's probably some changes needed but its ready for a look from a design PoV.
cc @mawinter69 |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 37 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| rbas.doAddTemplate(templateName, permIds, overwrite); | ||
| } | ||
|
|
||
| rsp.sendRedirect(redirectUrl); |
There was a problem hiding this comment.
handleTemplateSubmit calls RoleBasedAuthorizationStrategy#doAddTemplate(...), which is an HTTP handler that may write an error response (e.g. 400 on duplicate template name) via Stapler.getCurrentResponse2().sendError(...). This method then unconditionally redirects, which can mask the error or attempt to redirect after the response is committed.
Consider moving the template creation logic into RoleStrategyConfig (or a shared non-HTTP helper) so you can return a proper error to the dialog, and only redirect on success.
| rsp.sendRedirect(redirectUrl); | |
| if (!rsp.isCommitted()) { | |
| rsp.sendRedirect(redirectUrl); | |
| } |
| class="jenkins-button jenkins-button--tertiary rsp-card__action rsp-template-edit" | ||
| tooltip="${%Edit template}" | ||
| data-type="dialog-opener" | ||
| data-dialog-url="${rootURL}/manage/role-strategy/edit-template-dialog?templateName=${h.escape(template.name)}"> |
There was a problem hiding this comment.
The dialog URL embeds template.name in a query string using h.escape(...), which HTML-escapes but does not URL-encode. Template names containing spaces, &, ?, etc. will produce a broken URL (and can lead to confusing request parameters).
URL-encode the query parameter value (e.g., with a URL-encoding helper) rather than HTML-escaping it.
| data-dialog-url="${rootURL}/manage/role-strategy/edit-template-dialog?templateName=${h.escape(template.name)}"> | |
| data-dialog-url="${rootURL}/manage/role-strategy/edit-template-dialog?templateName=${h.urlEncode(template.name)}"> |
| <button type="button" class="jenkins-button jenkins-button--tertiary rsp-card__action rsp-card__edit" | ||
| tooltip="${%Edit role}" | ||
| data-type="dialog-opener" | ||
| data-dialog-url="${rootURL}/manage/role-strategy/edit-role-dialog?roleName=${h.escape(role.name)}&scope=${roleType}"> |
There was a problem hiding this comment.
The dialog URL embeds role.name in the query string using h.escape(...), which HTML-escapes but does not URL-encode. Role names containing spaces or reserved URL characters will break the generated URL.
URL-encode query parameter values instead of HTML-escaping them.
| data-dialog-url="${rootURL}/manage/role-strategy/edit-role-dialog?roleName=${h.escape(role.name)}&scope=${roleType}"> | |
| data-dialog-url="${rootURL}/manage/role-strategy/edit-role-dialog?roleName=${h.urlEncode(role.name)}&scope=${roleType}"> |
| card.remove(); | ||
| rspUpdateCardBorders(); | ||
| tplDelete(name) | ||
| .then(() => { | ||
| if ( | ||
| document.querySelectorAll("#rsp-template-cards .rsp-card") | ||
| .length === 0 | ||
| ) { | ||
| window.location.reload(); | ||
| } else { | ||
| notificationBar.show( | ||
| `Template "${name}" deleted`, | ||
| notificationBar.SUCCESS, | ||
| ); | ||
| } | ||
| }) | ||
| .catch((err) => { | ||
| notificationBar.show( | ||
| "Failed to save: " + err.message, | ||
| notificationBar.ERROR, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
The delete flow removes the template card from the DOM before the server-side delete succeeds. If the tplDelete(...) request fails, the UI stays out of sync (the template appears deleted but still exists server-side) and the user must reload to recover.
Consider only removing the card after a successful delete, or implement a rollback path on failure (re-insert the card / reload on error).
| * @param role The role to check | ||
| * @param sid The user or group name | ||
| * @param type "USER" or "GROUP" | ||
| * @return true if the sid is assigned to the role | ||
| */ | ||
| public boolean isAssigned(@NonNull Role role, @NonNull String sid, @NonNull String type) { | ||
| Set<PermissionEntry> entries = this.grantedRoles.get(role); | ||
| if (entries == null) { | ||
| return false; | ||
| } | ||
| AuthorizationType authType = "GROUP".equals(type) ? AuthorizationType.GROUP : AuthorizationType.USER; | ||
| return entries.contains(new PermissionEntry(authType, sid)) | ||
| || entries.contains(new PermissionEntry(AuthorizationType.EITHER, sid)); |
There was a problem hiding this comment.
RoleMap#isAssigned Javadoc says type is only "USER" or "GROUP", but callers in this PR also pass "EITHER". The implementation treats any non-"GROUP" as USER and then checks USER or EITHER entries, so it works, but the documentation is now inaccurate.
Update the Javadoc to include "EITHER" (or clarify how it is handled).
| @CheckForNull | ||
| private static JSONObject getSubmittedFormOrRedirect( | ||
| StaplerRequest2 req, StaplerResponse2 rsp, String redirectUrl) throws IOException { | ||
| req.setCharacterEncoding("UTF-8"); | ||
| JSONObject json = req.getSubmittedForm(); | ||
| JSONObject rolesMapping; | ||
| if (json.has("submit")) { | ||
| String rm = json.getString("rolesMapping"); | ||
| rolesMapping = JSONObject.fromObject(rm); | ||
| } else { | ||
| rolesMapping = json.getJSONObject("rolesMapping"); | ||
| try { | ||
| return req.getSubmittedForm(); | ||
| } catch (Exception e) { | ||
| rsp.sendRedirect(redirectUrl); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
getSubmittedFormOrRedirect swallows all getSubmittedForm() failures and issues a redirect. For endpoints like doAddRoleSubmit / doAssignRoleSubmit / template submits, an invalid or empty POST will likely become a 302 (often followed to 200 by the client) rather than a clear 400 error. This also makes the submit endpoints behave more like navigational pages than form APIs.
Consider returning a 400 (sendError) with a useful message when the form JSON is missing/invalid (and only redirect for genuine navigation flows), so both dialogs and tests can reliably detect bad submissions.
| var escapeHTML = function (unsafe) { | ||
| return unsafe.replace(/[&<>"']/g, function(m) { | ||
| switch (m) { | ||
| case "&": | ||
| return "&"; | ||
| case "<": | ||
| return "<"; | ||
| case '"': | ||
| return """; | ||
| default: | ||
| return "'"; | ||
| } |
There was a problem hiding this comment.
escapeHTML currently maps the > character to ' (apostrophe) because there is no explicit case '>'. This will corrupt output containing > and also fails to properly HTML-escape >.
Add an explicit case '>' -> '>' (and keep the apostrophe mapping only for ').
| if (permsJson != null) { | ||
| for (String rawKey : permsJson.keySet()) { | ||
| if (permsJson.optBoolean(rawKey, false)) { | ||
| Permission p = Permission.fromId(rawKey); |
There was a problem hiding this comment.
collectPermissionsFromFlat calls Permission.fromId(rawKey) directly, but the checkbox names in the edit-role dialog are of the form name="[${p.id}]". That means the JSON keys are likely bracketed, so Permission.fromId will return null and editing a role can unintentionally clear all permissions.
Strip the surrounding brackets (reuse stripBrackets) before calling Permission.fromId, consistent with collectPermissionsFromScoped/collectPermissionIds.
| Permission p = Permission.fromId(rawKey); | |
| String permId = stripBrackets(rawKey); | |
| Permission p = Permission.fromId(permId); |
Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com>
| @@ -0,0 +1,201 @@ | |||
| #!/bin/bash | |||
There was a problem hiding this comment.
Useful script for performance testing
| </div> | ||
| </f:entry> | ||
|
|
||
| <f:bottomButtonBar> |
There was a problem hiding this comment.
| <f:bottomButtonBar> | |
| <f:bottomButtonBar borderless="true"> |
| </div> | ||
| </f:entry> | ||
|
|
||
| <f:bottomButtonBar> |
There was a problem hiding this comment.
| <f:bottomButtonBar> | |
| <f:bottomButtonBar borderless="true"> |
|
|
||
| </f:entry> | ||
|
|
||
| <f:bottomButtonBar> |
There was a problem hiding this comment.
| <f:bottomButtonBar> | |
| <f:bottomButtonBar borderless="true"> |
|
|
||
| </f:entry> | ||
|
|
||
| <f:bottomButtonBar> |
There was a problem hiding this comment.
| <f:bottomButtonBar> | |
| <f:bottomButtonBar borderless="true"> |
| </div> | ||
| </f:entry> | ||
|
|
||
| <f:bottomButtonBar> |
There was a problem hiding this comment.
| <f:bottomButtonBar> | |
| <f:bottomButtonBar borderless="true"> |
| </div> | ||
| </f:entry> | ||
|
|
||
| <f:bottomButtonBar> |
There was a problem hiding this comment.
| <f:bottomButtonBar> | |
| <f:bottomButtonBar borderless="true"> |
|
|
Seems in Matrix auth you have added small |
|
In Matrix auth the permissions are directly editable. Maybe we can do that here as well (unless a template is used) and limit the edit dialog to item and agent roles to modify the pattern / template only. |
|
Handling for ambiguous assignments seems to be missing |
|
Yes because saving is done when the security page as a whole is saved, whereas here its saved when the dialog closes. It seemed clearer in this case to simplify everything by saving as part of the dialog, rather than saving every object on the page when you likely aren't changing much of it when adding e.g. new roles. |
That would be an argument against my comment about being able to directly modify the permissions on the page. |

TODO:
For later:
role-strategy.mov
Testing done
Extensively manually tested including with different personas:
Verified behaviour is correct across all of these.
I intend to create some playwright tests as well as there's 0 UI coverage at all in this repo (or in ATH), but that will likely be for another PR although I may stack it on this to get it going
Submitter checklist