mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Policies: scope to the owning team; editing restricted to team leaders (#6632)
This commit is contained in:
+32
@@ -0,0 +1,32 @@
|
||||
package stirling.software.saas.security;
|
||||
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.policy.config.PolicyManagementAuthority;
|
||||
|
||||
/**
|
||||
* SaaS policy context: only the LEADER of the current user's team may edit policies, and every user
|
||||
* is scoped to their own team. Replaces the self-hosted global-admin check, which is meaningless on
|
||||
* SaaS (a single global admin exists for the whole deployment, never per-org) — and the admin gets
|
||||
* no cross-team escape: scoping binds them like everyone else.
|
||||
*/
|
||||
@Component
|
||||
@Profile("saas")
|
||||
@RequiredArgsConstructor
|
||||
public class TeamLeaderPolicyManagementAuthority implements PolicyManagementAuthority {
|
||||
|
||||
private final TeamSecurityExpressions teamSecurity;
|
||||
|
||||
@Override
|
||||
public boolean canEditPolicies() {
|
||||
return teamSecurity.isCurrentUserTeamLeader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long currentUserTeamId() {
|
||||
return teamSecurity.currentUserTeamId();
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,27 @@ public class TeamSecurityExpressions {
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/** Whether the current authenticated user is a {@code LEADER} of their own team. */
|
||||
public boolean isCurrentUserTeamLeader() {
|
||||
User currentUser = getCurrentUser();
|
||||
if (currentUser == null || currentUser.getTeam() == null) {
|
||||
return false;
|
||||
}
|
||||
return membershipRepository
|
||||
.findByTeamIdAndUserId(currentUser.getTeam().getId(), currentUser.getId())
|
||||
.map(membership -> membership.getRole() == TeamRole.LEADER)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/** The current authenticated user's team id, or {@code null} if unauthenticated / teamless. */
|
||||
public Long currentUserTeamId() {
|
||||
User currentUser = getCurrentUser();
|
||||
if (currentUser == null || currentUser.getTeam() == null) {
|
||||
return null;
|
||||
}
|
||||
return currentUser.getTeam().getId();
|
||||
}
|
||||
|
||||
/** Whether the current authenticated user is any kind of member of the given team. */
|
||||
public boolean isTeamMember(Long teamId) {
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package stirling.software.saas.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/** SaaS policy context: team leader may edit; scoping uses the user's team. */
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TeamLeaderPolicyManagementAuthorityTest {
|
||||
|
||||
@Mock private TeamSecurityExpressions teamSecurity;
|
||||
|
||||
private TeamLeaderPolicyManagementAuthority authority() {
|
||||
return new TeamLeaderPolicyManagementAuthority(teamSecurity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void teamLeaderMayEditPolicies() {
|
||||
when(teamSecurity.isCurrentUserTeamLeader()).thenReturn(true);
|
||||
assertTrue(authority().canEditPolicies());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonLeaderMayNot() {
|
||||
when(teamSecurity.isCurrentUserTeamLeader()).thenReturn(false);
|
||||
assertFalse(authority().canEditPolicies());
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentUserTeamIdDelegatesToTeamSecurity() {
|
||||
when(teamSecurity.currentUserTeamId()).thenReturn(9L);
|
||||
assertEquals(9L, authority().currentUserTeamId());
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package stirling.software.saas.security;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import stirling.software.common.model.enumeration.TeamRole;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
import stirling.software.saas.model.TeamMembership;
|
||||
import stirling.software.saas.repository.TeamMembershipRepository;
|
||||
|
||||
/**
|
||||
* {@link TeamSecurityExpressions#isCurrentUserTeamLeader()} — used to gate policy editing on SaaS.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TeamSecurityExpressionsTest {
|
||||
|
||||
@Mock private TeamMembershipRepository membershipRepository;
|
||||
@Mock private UserService userService;
|
||||
|
||||
private static final long TEAM_ID = 2L;
|
||||
private static final long USER_ID = 1L;
|
||||
|
||||
private TeamSecurityExpressions expressions() {
|
||||
return new TeamSecurityExpressions(membershipRepository, userService);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearContext() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
private void authenticateAsUserWithTeam(boolean hasTeam) {
|
||||
User user = new User();
|
||||
user.setId(USER_ID);
|
||||
if (hasTeam) {
|
||||
Team team = new Team();
|
||||
team.setId(TEAM_ID);
|
||||
user.setTeam(team);
|
||||
}
|
||||
// API-key auth path: the principal is the User entity itself.
|
||||
SecurityContextHolder.getContext()
|
||||
.setAuthentication(new UsernamePasswordAuthenticationToken(user, null, List.of()));
|
||||
}
|
||||
|
||||
private TeamMembership membershipWithRole(TeamRole role) {
|
||||
TeamMembership membership = new TeamMembership();
|
||||
membership.setRole(role);
|
||||
return membership;
|
||||
}
|
||||
|
||||
@Test
|
||||
void leaderOfOwnTeamIsLeader() {
|
||||
authenticateAsUserWithTeam(true);
|
||||
when(membershipRepository.findByTeamIdAndUserId(TEAM_ID, USER_ID))
|
||||
.thenReturn(Optional.of(membershipWithRole(TeamRole.LEADER)));
|
||||
assertTrue(expressions().isCurrentUserTeamLeader());
|
||||
}
|
||||
|
||||
@Test
|
||||
void regularMemberIsNotLeader() {
|
||||
authenticateAsUserWithTeam(true);
|
||||
when(membershipRepository.findByTeamIdAndUserId(TEAM_ID, USER_ID))
|
||||
.thenReturn(Optional.of(membershipWithRole(TeamRole.MEMBER)));
|
||||
assertFalse(expressions().isCurrentUserTeamLeader());
|
||||
}
|
||||
|
||||
@Test
|
||||
void noMembershipIsNotLeader() {
|
||||
authenticateAsUserWithTeam(true);
|
||||
when(membershipRepository.findByTeamIdAndUserId(TEAM_ID, USER_ID))
|
||||
.thenReturn(Optional.empty());
|
||||
assertFalse(expressions().isCurrentUserTeamLeader());
|
||||
}
|
||||
|
||||
@Test
|
||||
void userWithoutTeamIsNotLeader() {
|
||||
authenticateAsUserWithTeam(false);
|
||||
assertFalse(expressions().isCurrentUserTeamLeader());
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentUserTeamIdReturnsTheUsersTeam() {
|
||||
authenticateAsUserWithTeam(true);
|
||||
assertEquals(TEAM_ID, expressions().currentUserTeamId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentUserTeamIdIsNullWithoutTeam() {
|
||||
authenticateAsUserWithTeam(false);
|
||||
assertNull(expressions().currentUserTeamId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unauthenticatedIsNotLeader() {
|
||||
// No authentication set on the context.
|
||||
lenient()
|
||||
.when(membershipRepository.findByTeamIdAndUserId(TEAM_ID, USER_ID))
|
||||
.thenReturn(Optional.of(membershipWithRole(TeamRole.LEADER)));
|
||||
assertFalse(expressions().isCurrentUserTeamLeader());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user