mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-02-08 11:59:13 +00:00
impersonation implementado
This commit is contained in:
12825
logs/erp.log
12825
logs/erp.log
File diff suppressed because one or more lines are too long
@ -149,6 +149,10 @@ public class SecurityConfig {
|
|||||||
"/pagos/redsys/**"
|
"/pagos/redsys/**"
|
||||||
)
|
)
|
||||||
.permitAll()
|
.permitAll()
|
||||||
|
.requestMatchers("/impersonate/exit")
|
||||||
|
.hasRole("PREVIOUS_ADMINISTRATOR")
|
||||||
|
.requestMatchers("/impersonate")
|
||||||
|
.hasAnyRole("SUPERADMIN", "ADMIN")
|
||||||
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
||||||
.anyRequest().authenticated())
|
.anyRequest().authenticated())
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,115 @@
|
|||||||
|
package com.imprimelibros.erp.users;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
|
||||||
|
import com.imprimelibros.erp.config.Sanitizer;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class ImpersonationController {
|
||||||
|
|
||||||
|
private static final String PREVIOUS_ADMIN_ROLE = "ROLE_PREVIOUS_ADMINISTRATOR";
|
||||||
|
private static final String SESSION_ATTR = "IMPERSONATOR_AUTH";
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
private final Sanitizer sanitizer;
|
||||||
|
|
||||||
|
public ImpersonationController(UserService userService, Sanitizer sanitizer) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.sanitizer = sanitizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/impersonate")
|
||||||
|
@PreAuthorize("hasRole('ADMIN') or hasRole('SUPERADMIN')")
|
||||||
|
public String impersonate(
|
||||||
|
@RequestParam("username") String username,
|
||||||
|
Authentication authentication,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
return "redirect:/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRole(authentication, PREVIOUS_ADMIN_ROLE)) {
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = sanitizer.plain(username);
|
||||||
|
if (normalized == null || normalized.isBlank()) {
|
||||||
|
return "redirect:/users";
|
||||||
|
}
|
||||||
|
normalized = normalized.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (authentication.getName() != null
|
||||||
|
&& authentication.getName().equalsIgnoreCase(normalized)) {
|
||||||
|
return "redirect:/users";
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDetails target;
|
||||||
|
try {
|
||||||
|
target = userService.loadUserByUsername(normalized);
|
||||||
|
} catch (UsernameNotFoundException ex) {
|
||||||
|
throw new AccessDeniedException("No autorizado");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean currentIsSuperAdmin = hasRole(authentication, "ROLE_SUPERADMIN");
|
||||||
|
boolean targetIsSuperAdmin = target.getAuthorities().stream()
|
||||||
|
.anyMatch(a -> "ROLE_SUPERADMIN".equals(a.getAuthority()));
|
||||||
|
if (targetIsSuperAdmin && !currentIsSuperAdmin) {
|
||||||
|
throw new AccessDeniedException("No autorizado");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpSession session = request.getSession(true);
|
||||||
|
if (session.getAttribute(SESSION_ATTR) == null) {
|
||||||
|
session.setAttribute(SESSION_ATTR, authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<GrantedAuthority> authorities = new ArrayList<>(target.getAuthorities());
|
||||||
|
authorities.add(new SimpleGrantedAuthority(PREVIOUS_ADMIN_ROLE));
|
||||||
|
|
||||||
|
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(
|
||||||
|
target, target.getPassword(), authorities);
|
||||||
|
newAuth.setDetails(authentication.getDetails());
|
||||||
|
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(newAuth);
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/impersonate/exit")
|
||||||
|
@PreAuthorize("hasRole('PREVIOUS_ADMINISTRATOR')")
|
||||||
|
public String exit(HttpServletRequest request) {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session != null) {
|
||||||
|
Object previous = session.getAttribute(SESSION_ATTR);
|
||||||
|
if (previous instanceof Authentication previousAuth) {
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(previousAuth);
|
||||||
|
} else {
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
}
|
||||||
|
session.removeAttribute(SESSION_ATTR);
|
||||||
|
}
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasRole(Authentication auth, String role) {
|
||||||
|
return auth != null
|
||||||
|
&& auth.getAuthorities().stream()
|
||||||
|
.anyMatch(a -> role.equals(a.getAuthority()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -81,6 +81,9 @@ public class UserController {
|
|||||||
"usuarios.delete.button",
|
"usuarios.delete.button",
|
||||||
"app.yes",
|
"app.yes",
|
||||||
"app.cancelar",
|
"app.cancelar",
|
||||||
|
"usuarios.impersonate.title",
|
||||||
|
"usuarios.impersonate.text",
|
||||||
|
"usuarios.impersonate.button",
|
||||||
"usuarios.delete.ok.title",
|
"usuarios.delete.ok.title",
|
||||||
"usuarios.delete.ok.text");
|
"usuarios.delete.ok.text");
|
||||||
|
|
||||||
@ -132,26 +135,36 @@ public class UserController {
|
|||||||
.collect(Collectors.joining(" ")))
|
.collect(Collectors.joining(" ")))
|
||||||
.add("actions", (user) -> {
|
.add("actions", (user) -> {
|
||||||
|
|
||||||
boolean isSuperAdmin = authentication.getAuthorities().stream()
|
boolean isSuperAdmin = authentication != null && authentication.getAuthorities().stream()
|
||||||
.anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN"));
|
.anyMatch(a -> a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||||
|
|
||||||
if (!isSuperAdmin) {
|
boolean isSelf = authentication != null
|
||||||
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
&& authentication.getName() != null
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
&& authentication.getName().equalsIgnoreCase(user.getUserName());
|
||||||
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
|
||||||
+
|
boolean targetIsSuperAdmin = user.getRoles().stream()
|
||||||
" </div>";
|
.anyMatch(r -> "SUPERADMIN".equalsIgnoreCase(r.getName()));
|
||||||
} else {
|
|
||||||
// Admin editando otro admin o usuario normal: puede editarse y eliminarse
|
StringBuilder actions = new StringBuilder();
|
||||||
return "<div class=\"hstack gap-3 flex-wrap\">\n" +
|
actions.append("<div class=\"hstack gap-3 flex-wrap\">");
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
actions.append("<a href=\"javascript:void(0);\" data-id=\"")
|
||||||
+ "\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>\n"
|
.append(user.getId())
|
||||||
+
|
.append("\" class=\"link-success btn-edit-user fs-15\"><i class=\"ri-edit-2-line\"></i></a>");
|
||||||
" <a href=\"javascript:void(0);\" data-id=\"" + user.getId()
|
|
||||||
+ "\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>\n"
|
if (!isSelf && (isSuperAdmin || !targetIsSuperAdmin)) {
|
||||||
+
|
actions.append("<a href=\"javascript:void(0);\" data-username=\"")
|
||||||
" </div>";
|
.append(user.getUserName())
|
||||||
|
.append("\" class=\"link-info btn-impersonate-user fs-15\"><i class=\"ri-user-shared-line\"></i></a>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
actions.append("<a href=\"javascript:void(0);\" data-id=\"")
|
||||||
|
.append(user.getId())
|
||||||
|
.append("\" class=\"link-danger btn-delete-user fs-15\"><i class=\"user-delete ri-delete-bin-line\"></i></a>");
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.append("</div>");
|
||||||
|
return actions.toString();
|
||||||
})
|
})
|
||||||
.where(base)
|
.where(base)
|
||||||
// Filtros custom:
|
// Filtros custom:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
spring.application.name=erp
|
spring.application.name=erp
|
||||||
# Active profile
|
# Active profile
|
||||||
#spring.profiles.active=dev
|
spring.profiles.active=dev
|
||||||
spring.profiles.active=test
|
#spring.profiles.active=test
|
||||||
#spring.profiles.active=prod
|
#spring.profiles.active=prod
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,4 +6,5 @@ app.cancelar=Cancel
|
|||||||
app.guardar=Save
|
app.guardar=Save
|
||||||
app.editar=Edit
|
app.editar=Edit
|
||||||
app.eliminar=Delete
|
app.eliminar=Delete
|
||||||
app.imprimir=Print
|
app.imprimir=Print
|
||||||
|
app.impersonate.exit=Return to my user
|
||||||
|
|||||||
@ -32,4 +32,5 @@ app.sidebar.gestion-pagos=Gestión de Pagos
|
|||||||
|
|
||||||
app.errors.403=No tienes permiso para acceder a esta página.
|
app.errors.403=No tienes permiso para acceder a esta página.
|
||||||
|
|
||||||
app.validation.required=Campo obligatorio
|
app.validation.required=Campo obligatorio
|
||||||
|
app.impersonate.exit=Volver a mi usuario
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
|
usuarios.impersonate.title=Sign in as user
|
||||||
|
usuarios.impersonate.text=You are about to sign in as <b>{0}</b>. You can return to your user from the menu.
|
||||||
|
usuarios.impersonate.button=Continue
|
||||||
|
|||||||
@ -53,4 +53,7 @@ usuarios.delete.title=Eliminar usuario
|
|||||||
usuarios.delete.button=Si, ELIMINAR
|
usuarios.delete.button=Si, ELIMINAR
|
||||||
usuarios.delete.text=¿Está seguro de que desea eliminar al usuario?<br>Esta acción no se puede deshacer.
|
usuarios.delete.text=¿Está seguro de que desea eliminar al usuario?<br>Esta acción no se puede deshacer.
|
||||||
usuarios.delete.ok.title=Usuario eliminado
|
usuarios.delete.ok.title=Usuario eliminado
|
||||||
usuarios.delete.ok.text=El usuario ha sido eliminado con éxito.
|
usuarios.delete.ok.text=El usuario ha sido eliminado con éxito.
|
||||||
|
usuarios.impersonate.title=Entrar como usuario
|
||||||
|
usuarios.impersonate.text=Vas a iniciar sesión como <b>{0}</b>. Podrás volver a tu usuario desde el menú.
|
||||||
|
usuarios.impersonate.button=Entrar
|
||||||
|
|||||||
@ -147,6 +147,55 @@ $(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Botón "Entrar como"
|
||||||
|
$(document).on('click', '.btn-impersonate-user', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const username = $(this).data('username');
|
||||||
|
|
||||||
|
const title = window.languageBundle.get(['usuarios.impersonate.title']) || 'Entrar como usuario';
|
||||||
|
const textTpl = window.languageBundle.get(['usuarios.impersonate.text'])
|
||||||
|
|| 'Vas a iniciar sesión como <b>{0}</b>.';
|
||||||
|
const confirmText = window.languageBundle.get(['usuarios.impersonate.button']) || 'Entrar';
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title,
|
||||||
|
html: textTpl.replace('{0}', username),
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-info w-xs mt-2',
|
||||||
|
cancelButton: 'btn btn-light w-xs mt-2'
|
||||||
|
},
|
||||||
|
confirmButtonText: confirmText,
|
||||||
|
cancelButtonText: window.languageBundle.get(['app.cancelar']) || 'Cancelar',
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/impersonate',
|
||||||
|
type: 'POST',
|
||||||
|
data: { username },
|
||||||
|
success: function () {
|
||||||
|
window.location.href = '/';
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
const msg = (xhr.responseJSON && xhr.responseJSON.message)
|
||||||
|
|| 'No se pudo iniciar sesión como ese usuario.';
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'error',
|
||||||
|
title: 'No se pudo suplantar',
|
||||||
|
text: msg,
|
||||||
|
buttonsStyling: false,
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-secondary me-2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Submit del form en el modal
|
// Submit del form en el modal
|
||||||
$(document).on('submit', '#userForm', function (e) {
|
$(document).on('submit', '#userForm', function (e) {
|
||||||
|
|||||||
@ -103,6 +103,14 @@
|
|||||||
<a class="dropdown-item" href="/apps-chat"><i
|
<a class="dropdown-item" href="/apps-chat"><i
|
||||||
class="mdi mdi-message-text-outline text-muted fs-16 align-middle me-1"></i>
|
class="mdi mdi-message-text-outline text-muted fs-16 align-middle me-1"></i>
|
||||||
<span class="align-middle" th:text="#{app.mensajes}">Mensajes</span></a>
|
<span class="align-middle" th:text="#{app.mensajes}">Mensajes</span></a>
|
||||||
|
<div sec:authorize="hasRole('PREVIOUS_ADMINISTRATOR')">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a class="dropdown-item" href="#"
|
||||||
|
onclick="document.getElementById('exitImpersonationForm').submit(); return false;">
|
||||||
|
<i class="mdi mdi-account-switch text-muted fs-16 align-middle me-1"></i>
|
||||||
|
<span class="align-middle" th:text="#{app.impersonate.exit}">Volver a mi usuario</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="#"
|
<a class="dropdown-item" href="#"
|
||||||
onclick="document.getElementById('logoutForm').submit(); return false;">
|
onclick="document.getElementById('logoutForm').submit(); return false;">
|
||||||
@ -127,7 +135,10 @@
|
|||||||
<form id="logoutForm" th:action="@{/logout}" method="post" class="d-none">
|
<form id="logoutForm" th:action="@{/logout}" method="post" class="d-none">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
</form>
|
</form>
|
||||||
|
<form id="exitImpersonationForm" th:action="@{/impersonate/exit}" method="post" class="d-none">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
</form>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user