From 46887ad8e57a23c563596974403d4d950ab61d76 Mon Sep 17 00:00:00 2001 From: Edward Date: Wed, 8 Oct 2025 13:26:42 +0200 Subject: [PATCH 1/3] CANS-116: Allow moderators to log in as users --- modules/mod_ginger_base/mod_ginger_base.erl | 23 ++++++++++++++- .../templates/_admin_edit_basics_user.tpl | 29 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 modules/mod_ginger_base/templates/_admin_edit_basics_user.tpl diff --git a/modules/mod_ginger_base/mod_ginger_base.erl b/modules/mod_ginger_base/mod_ginger_base.erl index ae064cb3..dbd7c8d6 100644 --- a/modules/mod_ginger_base/mod_ginger_base.erl +++ b/modules/mod_ginger_base/mod_ginger_base.erl @@ -7,7 +7,7 @@ -mod_title("Ginger Base"). -mod_description("Ginger Base"). -mod_prio(250). --mod_depends([mod_content_groups, mod_acl_user_groups]). +-mod_depends([mod_content_groups, mod_acl_user_groups, mod_admin_identity]). -mod_schema(13). @@ -198,6 +198,27 @@ manage_schema(_Version, Context) -> %%observe_acl_is_allowed(#acl_is_allowed{}, _Context) -> %% undefined. + +%% @doc Allow mod_admin_identity managers to impersonate other users +event(#postback{message={switch_user, [{id, Id}]}}, Context) -> + CanSwitch = z_acl:is_admin(Context) + orelse z_acl:is_allowed(use, mod_admin_identity, Context), + case CanSwitch + andalso is_integer(Id) + andalso Id =/= 1 + of + true -> + {ok, NewContext} = z_auth:switch_user(Id, Context), + Url = case z_acl:is_allowed(use, mod_admin, NewContext) of + true -> + z_dispatcher:url_for(admin, NewContext); + false -> + <<"/">> + end, + z_render:wire({redirect, [{location, Url}]}, NewContext); + false -> + z_render:growl_error(?__("You are not allowed to switch users.", Context), Context) + end; %% @doc Handle the submit event of a new comment event(#submit{message={newcomment, Args}, form=FormId}, Context) -> ExtraActions = proplists:get_all_values(action, Args), diff --git a/modules/mod_ginger_base/templates/_admin_edit_basics_user.tpl b/modules/mod_ginger_base/templates/_admin_edit_basics_user.tpl new file mode 100644 index 00000000..ddba87a4 --- /dev/null +++ b/modules/mod_ginger_base/templates/_admin_edit_basics_user.tpl @@ -0,0 +1,29 @@ +
+ + + {% if m.acl.is_allowed.use.mod_admin_identity or id == m.acl.user %} +
+
+

{_ User actions _}

+
+ {% button class="btn btn-default" action={dialog_set_username_password id=id} text=_"Set username / password" %} +
+ + {% if (m.acl.is_admin or m.acl.is_allowed.use.mod_admin_identity) and m.identity[id].is_user and id != m.acl.user and id != 1 %} +
+ {% button class="btn btn-default" action={confirm text=_"Click OK to log on as this user. You will be redirected to the home page if this user has no rights to access the admin system." postback={switch_user id=id} delegate=`mod_ginger_base`} text=_"Log on as this user" %} +
+ {% endif %} + + {% if id /= 1 %} +
+ {% button class="btn btn-default" text=_"Delete Username" action={dialog_delete_username id=id on_success={slide_fade_out target=#tr.id}} %} +
+ {% endif %} +
+
+ {% endif %} + + {% all include "_admin_edit_basics_user_extra.tpl" %} + +
From ab506e6c55c79490b43deb361095b870ae76c743 Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 14 Oct 2025 18:34:26 +0200 Subject: [PATCH 2/3] CANS-116: Add support for config values regarding user_switch --- modules/mod_ginger_base/mod_ginger_base.erl | 63 ++++++++++++++++++- .../mod_ginger_base/support/ginger_config.erl | 4 +- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/modules/mod_ginger_base/mod_ginger_base.erl b/modules/mod_ginger_base/mod_ginger_base.erl index dbd7c8d6..c3014ec6 100644 --- a/modules/mod_ginger_base/mod_ginger_base.erl +++ b/modules/mod_ginger_base/mod_ginger_base.erl @@ -201,12 +201,19 @@ manage_schema(_Version, Context) -> %% @doc Allow mod_admin_identity managers to impersonate other users event(#postback{message={switch_user, [{id, Id}]}}, Context) -> - CanSwitch = z_acl:is_admin(Context) + IsAdmin = z_acl:is_admin(Context), + CanSwitch = IsAdmin orelse z_acl:is_allowed(use, mod_admin_identity, Context), - case CanSwitch + ImpersonationEnabled = is_impersonation_enabled(Context), + AdminAllowed = IsAdmin + andalso is_integer(Id) + andalso Id =/= 1, + RegularAllowed = ImpersonationEnabled + andalso CanSwitch andalso is_integer(Id) andalso Id =/= 1 - of + andalso not switching_between_moderators(Id, Context), + case AdminAllowed orelse RegularAllowed of true -> {ok, NewContext} = z_auth:switch_user(Id, Context), Url = case z_acl:is_allowed(use, mod_admin, NewContext) of @@ -288,6 +295,56 @@ event(#postback{message={map_infobox, _Args}}, Context) -> ), z_render:wire({script, [{script, JS}]}, Context). +switching_between_moderators(TargetId, Context) -> + case is_horizontal_impersonation_allowed(Context) of + true -> + false; + false -> + case z_acl:is_admin(Context) of + true -> + false; + false -> + case z_acl:user(Context) of + undefined -> + false; + TargetId -> + false; + SwitcherId when is_integer(SwitcherId) -> + SudoContext = z_acl:sudo(Context), + case m_rsc:rid(acl_user_group_moderators, SudoContext) of + undefined -> + false; + ModeratorGroupId -> + user_in_group(SwitcherId, ModeratorGroupId, SudoContext) + andalso user_in_group(TargetId, ModeratorGroupId, SudoContext) + end; + _ -> + false + end + end + end. + +user_in_group(UserId, GroupId, Context) when is_integer(UserId), is_integer(GroupId) -> + lists:member(GroupId, m_edge:objects(UserId, hasusergroup, Context)); +user_in_group(_, _, _) -> + false. + +is_impersonation_enabled(Context) -> + case m_config:get_value(mod_ginger_base, activate_impersonation, Context) of + undefined -> + false; + Value -> + z_utils:is_true(Value) + end. + +is_horizontal_impersonation_allowed(Context) -> + case m_config:get_value(mod_ginger_base, allow_horizontal_impersonation, Context) of + undefined -> + false; + Value -> + z_utils:is_true(Value) + end. + %% @doc When a resource is persisted in the admin, update granularity for %% granular date fields. observe_admin_rscform(#admin_rscform{}, Post, _Context) -> diff --git a/modules/mod_ginger_base/support/ginger_config.erl b/modules/mod_ginger_base/support/ginger_config.erl index cacfe940..bb4c99fc 100644 --- a/modules/mod_ginger_base/support/ginger_config.erl +++ b/modules/mod_ginger_base/support/ginger_config.erl @@ -36,5 +36,7 @@ get_config() -> {mod_l10n, timezone, <<"Europe/Berlin">>}, %% Allow ginger-embed elements {site, html_elt_extra, <<"embed,iframe,object,script,ginger-embed">>}, - {site, html_attr_extra, <<"data,allowfullscreen,flashvars,frameborder,scrolling,async,defer,data-rdf">>} + {site, html_attr_extra, <<"data,allowfullscreen,flashvars,frameborder,scrolling,async,defer,data-rdf">>}, + {mod_ginger_base, activate_impersonation, false}, + {mod_ginger_base, allow_horizontal_impersonation, false} ]. From c400ae7e723d20e4aa9cc02d889084b5e2a128e0 Mon Sep 17 00:00:00 2001 From: Edward Date: Wed, 15 Oct 2025 10:42:55 +0200 Subject: [PATCH 3/3] CANS-116: Add usergroup comaprison for switch_user --- modules/mod_ginger_base/mod_ginger_base.erl | 86 +++++++++++++-------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/modules/mod_ginger_base/mod_ginger_base.erl b/modules/mod_ginger_base/mod_ginger_base.erl index c3014ec6..2c874183 100644 --- a/modules/mod_ginger_base/mod_ginger_base.erl +++ b/modules/mod_ginger_base/mod_ginger_base.erl @@ -206,13 +206,11 @@ event(#postback{message={switch_user, [{id, Id}]}}, Context) -> orelse z_acl:is_allowed(use, mod_admin_identity, Context), ImpersonationEnabled = is_impersonation_enabled(Context), AdminAllowed = IsAdmin - andalso is_integer(Id) andalso Id =/= 1, RegularAllowed = ImpersonationEnabled andalso CanSwitch - andalso is_integer(Id) andalso Id =/= 1 - andalso not switching_between_moderators(Id, Context), + andalso can_impersonate_user(Id, Context), case AdminAllowed orelse RegularAllowed of true -> {ok, NewContext} = z_auth:switch_user(Id, Context), @@ -295,39 +293,63 @@ event(#postback{message={map_infobox, _Args}}, Context) -> ), z_render:wire({script, [{script, JS}]}, Context). -switching_between_moderators(TargetId, Context) -> - case is_horizontal_impersonation_allowed(Context) of - true -> +can_impersonate_user(TargetId, Context) -> + case z_acl:user(Context) of + undefined -> false; - false -> - case z_acl:is_admin(Context) of - true -> + TargetId -> + true; + SwitcherId when is_integer(SwitcherId) -> + AllowHorizontal = is_horizontal_impersonation_allowed(Context), + SudoContext = z_acl:sudo(Context), + SwitcherGroups = direct_user_groups(SwitcherId, SudoContext), + TargetGroups = direct_user_groups(TargetId, SudoContext), + case {SwitcherGroups, TargetGroups} of + {[], _} -> + false; + {_, []} -> false; - false -> - case z_acl:user(Context) of - undefined -> - false; - TargetId -> - false; - SwitcherId when is_integer(SwitcherId) -> - SudoContext = z_acl:sudo(Context), - case m_rsc:rid(acl_user_group_moderators, SudoContext) of - undefined -> - false; - ModeratorGroupId -> - user_in_group(SwitcherId, ModeratorGroupId, SudoContext) - andalso user_in_group(TargetId, ModeratorGroupId, SudoContext) - end; - _ -> - false - end - end + _ -> + lists:all( + fun(TargetGroup) -> + group_impersonation_allowed(TargetGroup, SwitcherGroups, SudoContext, AllowHorizontal) + end, + TargetGroups) + end; + _ -> + false + end. + +direct_user_groups(UserId, Context) -> + lists:usort(acl_user_groups_checks:has_user_groups(UserId, Context)). + +group_impersonation_allowed(TargetGroup, SwitcherGroups, Context, AllowHorizontal) -> + TargetPath = user_group_path(TargetGroup, Context), + TargetPath =/= [] andalso + lists:any( + fun(SwitcherGroup) -> + SwitcherPath = user_group_path(SwitcherGroup, Context), + path_allows(TargetPath, SwitcherPath, AllowHorizontal) + end, + SwitcherGroups). + +user_group_path(GroupId, Context) -> + case mod_acl_user_groups:lookup(GroupId, Context) of + undefined -> + [GroupId]; + Path when is_list(Path) -> + Path end. -user_in_group(UserId, GroupId, Context) when is_integer(UserId), is_integer(GroupId) -> - lists:member(GroupId, m_edge:objects(UserId, hasusergroup, Context)); -user_in_group(_, _, _) -> - false. +path_allows(TargetPath, SwitcherPath, AllowHorizontal) when is_list(TargetPath), is_list(SwitcherPath) -> + case lists:suffix(TargetPath, SwitcherPath) of + true when AllowHorizontal -> + true; + true -> + length(SwitcherPath) > length(TargetPath); + false -> + false + end. is_impersonation_enabled(Context) -> case m_config:get_value(mod_ginger_base, activate_impersonation, Context) of