Add Reset Password button to Edit User view - #70243
Conversation
1780b15 to
dbd37f4
Compare
dbd37f4 to
ca04383
Compare
|
The feature makes sense — having to bounce back to Show User just to reset a password is a genuine annoyance — and I checked the part that matters most here: there is no permission bypass. {% macro render_action_links(actions, pk, modelview_name) %}
{% set actions = actions | get_actions_on_show(modelview_name) %}so injecting the action into I'd like to suggest a different approach to the template, though, because copying
You are right that {% extends "appbuilder/general/model/edit.html" %}
{% import 'appbuilder/general/lib.html' as lib %}
{% block edit_form %}
{{ super() }}
{% if actions %}
<div class="well well-sm">
{{ lib.render_action_links(actions, pk, modelview_name) }}
</div>
{% endif %}
{% endblock %}and the view can pass the values straight to edit_template = "appbuilder/general/model/user_edit.html"
@expose("/edit/<pk>", methods=["GET", "POST"])
@has_access
def edit(self, pk):
pk = self._deserialize_pk_if_composite(pk)
widgets = self._edit(pk)
if not widgets:
return self.post_edit_redirect()
return self.render_template(
self.edit_template,
title=self.edit_title,
widgets=widgets,
related_views=self._related_views,
actions={"resetpasswords": self.actions.get("resetpasswords")},
pk=pk,
modelview_name=self.__class__.__name__,
)That drops the ~45 lines of duplicated form markup, removes the need for Two smaller notes: The description says the new template "extends FAB's built-in "Mirroring the existing Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting |
The Show User view already surfaces a Reset Password button via FAB's show-widget actions block, but the Edit User view has no equivalent, so admins have to navigate back to Show User just to reset a password. This wires the same, already-registered resetpasswords action into the Edit User page by giving CustomUserDBModelView its own edit widget and template that render the action link, mirroring the existing Show User override pattern. closes: apache#37030
The previous template copied FAB's form_vertical.html and rendered the action link inside the model's own <form>. render_action_links emits its own <form id="action_form">, so the page ended up with a form nested inside another form, which is invalid HTML and only happened to work because browsers silently drop the inner element. Overriding the edit_form block in appbuilder/general/model/edit.html instead puts the action after the model form closes, drops the duplicated form markup, and keeps the page in sync with any future change to FAB's own template.
The view previously reached into widgets["edit"].template_args to inject the Reset Password action; now that the action is rendered by the edit_form block instead of a widget, the view can pass it straight to render_template.
ca04383 to
5c6d0fd
Compare
|
Pushed a follow-up commit that switches the Edit User view to override Also corrected the PR description: the new template extends FAB's Existing tests ( |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 5 days if no further activity occurs. Thank you for your contributions. |
|
Hi Team, kindly look into this PR. Friendly Ping |
|
Any chance you can attached a screenshot to the description so that we can see the final result? |
potiuk
left a comment
There was a problem hiding this comment.
Approving. I traced the permission chain rather than taking the description's word for it, and it holds up: render_action_links → get_actions_on_show → is_item_visible, which resolves resetpasswords through method_permission_name to can_read and checks it against class_permission_name — a property that returns Users on the edit endpoint and Passwords on the action endpoint (user.py:61-70). Link visibility is Users-read, execution is Passwords-read, and both match what the Show view already does. No new permission surface, which is the thing that matters here.
Three more things I went looking for and didn't find problems with:
- The
edit()override is a faithful copy of FAB'sModelView.edit(views.py:253-264) with three extra kwargs. I specifically checked whetherform_actionwas dropped — FAB's base doesn't pass it either, so the widget renders exactly as upstream. self.actions.get("resetpasswords")mirrors FAB's ownUserDBModelView.show()(security/views.py:385-387) verbatim. I'd have flagged theNonecase if it weren't the upstream pattern.- The nested-form reasoning in your description is right:
{% block edit_form %}inedit.htmlisn't inside a<form>, so the action links land outside the model form.
Two follow-ups:
Could you attach a screenshot? The template asks for before/after on user-facing UI changes, and there's a substantive reason here: your <div class="well well-sm"> becomes a direct child of .tab-content that isn't a .tab-pane, so it should sit below the tab content and persist across tab switches. That's almost certainly what you intend, but it's a rendering question the tests can't answer — they only assert the string is present.
One test case worth adding — details inline on line 156. The two tests vary two permissions at once, so they don't pin the Users-read / Passwords-read split your own comment describes.
I've approved so this isn't waiting on another review round, but please address the inline comment and add the screenshot, and mark the thread resolved, before it merges. Ping me when they're done and I'll take the next look.
Nice work on the iteration history, incidentally: the widget-subclass approach, discovering the nested-<form> problem, and writing up why the page-level template is the right fix made this much faster to review than it would otherwise have been.
This review was drafted by an AI-assisted tool and confirmed by an Airflow maintainer. The maintainer approving this PR has read the findings and signed off. If something feels off, please reply on the PR and a maintainer will follow up.
More on how Airflow handles maintainer review: contributing-docs/05_pull_requests.rst.
Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting
The existing pair of tests varied Users read and Passwords read together, so they could not tell apart "link is gated on Users read" from "link is gated on Passwords read". Cover the seam explicitly: a user with Users read but without Passwords read sees the link, and following it is refused.
|
@potiuk the follow-ups are done: The missing test case is in b2a25d6: Users read without Passwords read shows the link, and following it is refused. The with-access test also asserts the same POST reaches the reset form. |


The Show User view already surfaces a Reset Password action button (via FAB's show-widget actions block), but the Edit User view has no equivalent, forcing admins to navigate back to Show User just to reset a password.
This wires the same, already-registered
resetpasswordsaction into the Edit User page:CustomUserDBModelViewgets a dedicatededit_template(appbuilder/general/model/user_edit.html) and overridesedit()to pass theresetpasswordsaction,pk, andmodelview_namestraight torender_template.appbuilder/general/model/edit.htmland overrides itsedit_formblock, calling{{ super() }}for the original form and then rendering the action link via the samerender_action_linksmacro used byshow.html, outside the model's own<form>tag. An earlier version of this PR copiedform_vertical.htmlinto a new widget instead, butrender_action_linksemits its own nested<form>, which produced invalid nested-form HTML when called from inside the edit form; extending the page-level template avoids that. No new routes or permissions were introduced — the fix reuses the existingresetpasswordsaction, route, and permission mapping, andrender_action_linksapplies its own permission filter before rendering anything.closes: #37030
Test plan
test_user_edit_view_shows_reset_password_action_with_access— asserts the Reset Password link renders on the Edit User page for a user with read/edit access to Users and read access to Passwords.test_user_edit_view_hides_reset_password_action_without_access— asserts the link is absent when the user lacks read access to Users.test_views_custom_user_views.py, 28 tests) plus the broaderproviders/fab/tests/unit/fab/www/views/andproviders/fab/tests/unit/fab/auth_manager/views/suites (46 tests total) — all passed, no regressions.prek run --from-ref upstream/main --stage pre-commitand--stage manual— passed.breeze run mypy providers/fab/src/airflow/providers/fab/auth_manager/views/user.py— passed.Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Sonnet 5) following the guidelines