Compare commits

...
Author SHA1 Message Date
Reece b33c089220 polish(policies): tighten comments on the team-scope/authority code
Trim verbose/history-framing comments to concise, timeless descriptions. No behaviour change.
2026-06-11 23:37:47 +01:00
Reece 0209bda04a fix(policies): scope policies to the owning team
Policies were globally shared: visible() returned every stored policy to every user, and access checks were removed (#6625's 'org-wide' model), so a team leader could see and edit policies belonging to other teams/orgs. Stamp each policy with the creating user's teamId (server-side, persisted in the policy JSON — no migration) and scope all access by it: list/get/run/edit/delete are restricted to the caller's team. This binds everyone, admins included — there is no cross-team escape. The edit-role gate (team leader) is unchanged and now composes with team scoping.

Current user's team is resolved per build via PolicyManagementAuthority.currentUserTeamId() (SaaS: team-security resolution; self-hosted: the user's team). Tests cover team-filtered visibility/access and the team-id resolution.
2026-06-11 23:14:24 +01:00
Reece ae66b8a5e1 fix(policies): gate policy editing to team leaders on SaaS, not admin
Builds on the org-wide policy model (#6625): reads/runs are open to all; create/edit/delete is gated by PolicyController.requirePolicyEditingAllowed, which was admin-only. On SaaS that's the wrong role — there is a single global admin for the whole deployment, never one per org — so org users couldn't edit policies at all. Introduce a PolicyManagementAuthority strategy: self-hosted keeps the global-admin check (AdminPolicyManagementAuthority); SaaS uses the leader of the user's team (TeamLeaderPolicyManagementAuthority -> TeamSecurityExpressions.isCurrentUserTeamLeader). The proprietary policy layer stays decoupled from the team model via the interface + profile-scoped beans.

Tests: TeamSecurityExpressionsTest (leader/member/no-membership/no-team/unauthenticated), plus delegation tests for both authority beans.
2026-06-11 22:59:00 +01:00
13 changed files with 432 additions and 48 deletions
@@ -0,0 +1,37 @@
package stirling.software.proprietary.policy.config;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.service.UserService;
/** Self-hosted policy authority: a global admin may edit; scoping uses the current user's team. */
@Component
@Profile("!saas")
@RequiredArgsConstructor
public class AdminPolicyManagementAuthority implements PolicyManagementAuthority {
private final UserService userService;
@Override
public boolean canEditPolicies() {
return userService.isCurrentUserAdmin();
}
@Override
public Long currentUserTeamId() {
String username = userService.getCurrentUsername();
if (username == null) {
return null;
}
return userService
.findByUsername(username)
.map(User::getTeam)
.map(Team::getId)
.orElse(null);
}
}
@@ -1,6 +1,7 @@
package stirling.software.proprietary.policy.config;
import java.util.List;
import java.util.Objects;
import org.springframework.stereotype.Component;
@@ -11,10 +12,9 @@ import stirling.software.common.service.UserServiceInterface;
import stirling.software.proprietary.policy.model.Policy;
/**
* Policies are org-wide: every user may view and run any stored policy, so reads and runs are open
* to all. Creating, editing, and deleting is gated to admins at the controller (see {@code
* PolicyController#requirePolicyEditingAllowed}). The owner is still recorded server-side (for run
* / usage attribution) but no longer restricts visibility or access.
* Scopes policy access to the caller's team: every user — admins included — may view, run, edit, or
* delete only policies stamped with their own team. Whether a user may edit at all is a separate
* check at the controller. Applies only when login is enabled; single-user deployments pass.
*/
@Component
@RequiredArgsConstructor
@@ -22,15 +22,33 @@ public class PolicyAccessGuard {
private final UserServiceInterface userService;
private final ApplicationProperties applicationProperties;
private final PolicyManagementAuthority policyManagementAuthority;
/** Owner for a new policy: the current user, or {@code null} when login is disabled. */
public String ownerForNewPolicy() {
return enforced() ? userService.getCurrentUsername() : null;
}
/** All stored policies are visible to every user (org-wide). */
/** Team a new policy is stamped with — the creator's team. {@code null} when login disabled. */
public Long teamForNewPolicy() {
return enforced() ? policyManagementAuthority.currentUserTeamId() : null;
}
/** Whether the policy belongs to the current user's team (so they may view/run/edit it). */
public boolean canAccess(Policy policy) {
if (!enforced()) {
return true;
}
return Objects.equals(policy.teamId(), policyManagementAuthority.currentUserTeamId());
}
/** The subset of {@code policies} scoped to the current user's team. */
public List<Policy> visible(List<Policy> policies) {
return policies;
if (!enforced()) {
return policies;
}
Long teamId = policyManagementAuthority.currentUserTeamId();
return policies.stream().filter(policy -> Objects.equals(policy.teamId(), teamId)).toList();
}
private boolean enforced() {
@@ -0,0 +1,19 @@
package stirling.software.proprietary.policy.config;
/**
* The current user's policy authority, pluggable per deployment so the policy layer needn't know
* the team model. Resolves who may edit policies (SaaS: a team leader; self-hosted: a global admin)
* and which team scopes them.
*/
public interface PolicyManagementAuthority {
/** Whether the current user may create, edit, or delete policies. */
boolean canEditPolicies();
/**
* The team that scopes the current user's policies: a new policy is stamped with it, and it is
* the only team whose policies the user may see, run, or edit. {@code null} when unresolved (no
* authenticated user or no team).
*/
Long currentUserTeamId();
}
@@ -36,10 +36,10 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.job.JobResponse;
import stirling.software.common.service.UserServiceInterface;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.proprietary.policy.config.PolicyAccessGuard;
import stirling.software.proprietary.policy.config.PolicyManagementAuthority;
import stirling.software.proprietary.policy.engine.PolicyRunHandle;
import stirling.software.proprietary.policy.engine.PolicyRunRegistry;
import stirling.software.proprietary.policy.engine.PolicyRunner;
@@ -72,7 +72,7 @@ public class PolicyController {
private final PolicyStore policyStore;
private final PolicyValidator policyValidator;
private final PolicyAccessGuard policyAccessGuard;
private final UserServiceInterface userService;
private final PolicyManagementAuthority policyManagementAuthority;
private final ApplicationProperties applicationProperties;
private final TempFileManager tempFileManager;
@@ -165,23 +165,29 @@ public class PolicyController {
}
/**
* Assign the owner: create stamps the current user; update preserves the existing owner — so
* the client can neither forge ownership on create nor reassign it on update. Editing is gated
* to admins by {@link #requirePolicyEditingAllowed}; policies are org-wide, so there is no
* per-owner access check here.
* Assign owner and team server-side: create stamps the current user and their team; update
* keeps the existing owner and team after checking the policy belongs to the caller's team. The
* client can't forge ownership on create or reach across teams on update (cross-team reads as
* not-found).
*/
private Policy resolveOwnership(Policy incoming) {
String id = incoming.id();
if (id != null && !id.isBlank()) {
Policy existing = policyStore.get(id).orElse(null);
if (existing != null) {
return withOwner(incoming, existing.owner());
if (!policyAccessGuard.canAccess(existing)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No policy: " + id);
}
return withOwnerAndTeam(incoming, existing.owner(), existing.teamId());
}
}
return withOwner(incoming, policyAccessGuard.ownerForNewPolicy());
return withOwnerAndTeam(
incoming,
policyAccessGuard.ownerForNewPolicy(),
policyAccessGuard.teamForNewPolicy());
}
private static Policy withOwner(Policy policy, String owner) {
private static Policy withOwnerAndTeam(Policy policy, String owner, Long teamId) {
return new Policy(
policy.id(),
policy.name(),
@@ -190,32 +196,32 @@ public class PolicyController {
policy.trigger(),
policy.sources(),
policy.steps(),
policy.output());
policy.output(),
teamId);
}
/**
* Creating, editing, pausing/resuming, and deleting policies is admin-only on multi-user
* deployments. Every mutation routes through {@link #savePolicy} (pause/resume re-save with a
* flipped {@code enabled} flag) or {@link #deletePolicy}, so gating those two covers them all;
* runs ({@code /run}) stay open. Single-user deployments (login disabled) have no admin
* concept, so they trust the local operator. The path allowlist for folder sources/outputs is
* enforced separately by {@link PolicyValidator} at validation time.
* Gate create/edit/delete to the policy-editor role ({@link PolicyManagementAuthority}; a team
* leader on SaaS). Pause/resume re-saves through {@link #savePolicy} and deletion through
* {@link #deletePolicy}, so gating those covers every mutation; runs stay open. Single-user
* deployments (login disabled) trust the local operator. Which team's policies a user may touch
* is scoped separately by {@link PolicyAccessGuard}.
*/
private void requirePolicyEditingAllowed() {
if (!applicationProperties.getSecurity().isEnableLogin()) {
return;
}
if (!userService.isCurrentUserAdmin()) {
if (!policyManagementAuthority.canEditPolicies()) {
throw new ResponseStatusException(
HttpStatus.FORBIDDEN,
"Policies may only be created or modified by an administrator");
"Policies may only be created or modified by a team leader");
}
}
@GetMapping
@Operation(
summary = "List policies",
description = "Lists all policies (org-wide; every user sees them all).")
description = "Lists the policies belonging to the caller's team.")
public List<Policy> listPolicies() {
return policyAccessGuard.visible(policyStore.all());
}
@@ -225,6 +231,7 @@ public class PolicyController {
public ResponseEntity<Policy> getPolicy(@PathVariable String policyId) {
return policyStore
.get(policyId)
.filter(policyAccessGuard::canAccess)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
@@ -233,7 +240,10 @@ public class PolicyController {
@Operation(summary = "Delete a policy by id")
public ResponseEntity<Void> deletePolicy(@PathVariable String policyId) {
requirePolicyEditingAllowed();
if (policyStore.get(policyId).isPresent() && policyStore.delete(policyId)) {
// Scope to the caller's team: a policy in another team reads as not-found.
boolean accessible =
policyStore.get(policyId).filter(policyAccessGuard::canAccess).isPresent();
if (accessible && policyStore.delete(policyId)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
@@ -253,6 +263,7 @@ public class PolicyController {
Policy policy =
policyStore
.get(policyId)
.filter(policyAccessGuard::canAccess)
.orElseThrow(
() ->
new ResponseStatusException(
@@ -17,7 +17,8 @@ public record Policy(
TriggerConfig trigger,
List<InputSpec> sources,
List<PipelineStep> steps,
OutputSpec output) {
OutputSpec output,
Long teamId) {
public Policy {
sources = sources == null ? List.of() : List.copyOf(sources);
@@ -25,6 +26,19 @@ public record Policy(
output = output == null ? OutputSpec.inline() : output;
}
/** Without an owning team — for engine runs; stored policies are stamped with a team. */
public Policy(
String id,
String name,
String owner,
boolean enabled,
TriggerConfig trigger,
List<InputSpec> sources,
List<PipelineStep> steps,
OutputSpec output) {
this(id, name, owner, enabled, trigger, sources, steps, output, null);
}
/** A policy with no configured sources (a generator, or files supplied directly to a run). */
public Policy(
String id,
@@ -34,7 +48,7 @@ public record Policy(
TriggerConfig trigger,
List<PipelineStep> steps,
OutputSpec output) {
this(id, name, owner, enabled, trigger, List.of(), steps, output);
this(id, name, owner, enabled, trigger, List.of(), steps, output, null);
}
/** This policy's pipeline as the engine sees it. */
@@ -31,7 +31,8 @@ public class InProcessPolicyStore implements PolicyStore {
policy.trigger(),
policy.sources(),
policy.steps(),
policy.output());
policy.output(),
policy.teamId());
policies.put(id, stored);
return stored;
}
@@ -38,7 +38,8 @@ public class JpaPolicyStore implements PolicyStore {
policy.trigger(),
policy.sources(),
policy.steps(),
policy.output());
policy.output(),
policy.teamId());
PolicyEntity entity = new PolicyEntity();
entity.setId(id);
@@ -0,0 +1,58 @@
package stirling.software.proprietary.policy.config;
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.when;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.service.UserService;
/** Self-hosted policy context: a global admin may edit; scoping uses the current user's team. */
@ExtendWith(MockitoExtension.class)
class AdminPolicyManagementAuthorityTest {
@Mock private UserService userService;
private AdminPolicyManagementAuthority authority() {
return new AdminPolicyManagementAuthority(userService);
}
@Test
void adminMayEditPolicies() {
when(userService.isCurrentUserAdmin()).thenReturn(true);
assertTrue(authority().canEditPolicies());
}
@Test
void nonAdminMayNot() {
when(userService.isCurrentUserAdmin()).thenReturn(false);
assertFalse(authority().canEditPolicies());
}
@Test
void currentUserTeamIdResolvesFromTheCurrentUsersTeam() {
Team team = new Team();
team.setId(42L);
User user = new User();
user.setTeam(team);
when(userService.getCurrentUsername()).thenReturn("alice");
when(userService.findByUsername("alice")).thenReturn(Optional.of(user));
assertEquals(42L, authority().currentUserTeamId());
}
@Test
void currentUserTeamIdIsNullWhenNoCurrentUser() {
when(userService.getCurrentUsername()).thenReturn(null);
assertNull(authority().currentUserTeamId());
}
}
@@ -1,7 +1,9 @@
package stirling.software.proprietary.policy.config;
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.when;
import java.util.List;
@@ -17,47 +19,66 @@ import stirling.software.proprietary.policy.model.OutputSpec;
import stirling.software.proprietary.policy.model.Policy;
/**
* Tests for {@link PolicyAccessGuard}. Policies are org-wide: every user sees them all (no
* owner-based filtering). The owner is still assigned server-side — the current user when login is
* enabled, {@code null} otherwise — purely for run/usage attribution.
* {@link PolicyAccessGuard}: policies are scoped to the caller's team. A user sees/accesses only
* their own team's policies (admins included — there is no cross-team escape). Login disabled
* (single-user) bypasses scoping.
*/
@ExtendWith(MockitoExtension.class)
class PolicyAccessGuardTest {
@Mock private UserServiceInterface userService;
@Mock private PolicyManagementAuthority policyManagementAuthority;
private PolicyAccessGuard guard(boolean loginEnabled) {
ApplicationProperties properties = new ApplicationProperties();
properties.getSecurity().setEnableLogin(loginEnabled);
return new PolicyAccessGuard(userService, properties);
return new PolicyAccessGuard(userService, properties, policyManagementAuthority);
}
@Test
void visibleReturnsEveryPolicyWhenLoginEnabled() {
// Org-wide: a non-admin sees every policy, not just the ones they own.
List<Policy> all = List.of(ownedBy("alice"), ownedBy("bob"), ownedBy("alice"));
assertEquals(all, guard(true).visible(all));
void visibleFiltersToTheCallersTeam() {
when(policyManagementAuthority.currentUserTeamId()).thenReturn(1L);
List<Policy> all = List.of(inTeam(1L), inTeam(2L), inTeam(1L), inTeam(null));
List<Policy> visible = guard(true).visible(all);
assertEquals(2, visible.size());
assertTrue(visible.stream().allMatch(p -> Long.valueOf(1L).equals(p.teamId())));
}
@Test
void visibleReturnsEveryPolicyWhenLoginDisabled() {
List<Policy> all = List.of(ownedBy("alice"), ownedBy("bob"));
void visibleReturnsEverythingWhenLoginDisabled() {
List<Policy> all = List.of(inTeam(1L), inTeam(2L));
assertEquals(all, guard(false).visible(all));
}
@Test
void ownerForNewPolicyIsTheCurrentUserWhenLoginEnabled() {
when(userService.getCurrentUsername()).thenReturn("alice");
assertEquals("alice", guard(true).ownerForNewPolicy());
void canAccessOnlyOwnTeamsPolicy() {
when(policyManagementAuthority.currentUserTeamId()).thenReturn(1L);
assertTrue(guard(true).canAccess(inTeam(1L)));
assertFalse(guard(true).canAccess(inTeam(2L)));
assertFalse(guard(true).canAccess(inTeam(null)));
}
@Test
void ownerForNewPolicyIsNullWhenLoginDisabled() {
// Single-user deployment: no identity to attribute to.
assertNull(guard(false).ownerForNewPolicy());
void canAccessAnythingWhenLoginDisabled() {
assertTrue(guard(false).canAccess(inTeam(2L)));
}
private static Policy ownedBy(String owner) {
return new Policy("p1", "p", owner, true, null, List.of(), List.of(), OutputSpec.inline());
@Test
void ownerAndTeamForNewPolicyComeFromTheCurrentUserWhenLoginEnabled() {
when(userService.getCurrentUsername()).thenReturn("alice");
when(policyManagementAuthority.currentUserTeamId()).thenReturn(7L);
assertEquals("alice", guard(true).ownerForNewPolicy());
assertEquals(7L, guard(true).teamForNewPolicy());
}
@Test
void ownerAndTeamForNewPolicyAreNullWhenLoginDisabled() {
assertNull(guard(false).ownerForNewPolicy());
assertNull(guard(false).teamForNewPolicy());
}
private static Policy inTeam(Long teamId) {
return new Policy(
"p1", "p", "owner", true, null, List.of(), List.of(), OutputSpec.inline(), teamId);
}
}
@@ -0,0 +1,27 @@
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 authority: only a team leader may edit; scoping uses the current user's team. */
@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();
@@ -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());
}
}
@@ -0,0 +1,116 @@
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}: current-user team leadership and team-id resolution. */
@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());
}
}