mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
org init
This commit is contained in:
parent
0966b919cb
commit
5e5f2dda83
@ -10,10 +10,23 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
public enum Role {
|
||||
|
||||
// Unlimited access
|
||||
// System-wide administrator - can manage all organizations
|
||||
SYSTEM_ADMIN(
|
||||
"ROLE_SYSTEM_ADMIN",
|
||||
Integer.MAX_VALUE,
|
||||
Integer.MAX_VALUE,
|
||||
"adminUserSettings.systemAdmin"),
|
||||
|
||||
// Organization administrator - can manage their organization and all its teams
|
||||
ORG_ADMIN("ROLE_ORG_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.orgAdmin"),
|
||||
|
||||
// Team leader - can manage users in their specific team
|
||||
TEAM_LEAD("ROLE_TEAM_LEAD", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.teamLead"),
|
||||
|
||||
// Legacy admin role - equivalent to SYSTEM_ADMIN for backward compatibility
|
||||
ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.admin"),
|
||||
|
||||
// Unlimited access
|
||||
// Regular user with unlimited access within their team/org
|
||||
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.user"),
|
||||
|
||||
// 40 API calls Per Day, 40 web calls
|
||||
@ -63,4 +76,35 @@ public enum Role {
|
||||
}
|
||||
throw new IllegalArgumentException("No Role defined for id: " + roleId);
|
||||
}
|
||||
|
||||
/** Checks if this role can manage users across all organizations */
|
||||
public boolean isSystemAdmin() {
|
||||
return this == SYSTEM_ADMIN || this == ADMIN; // ADMIN for backward compatibility
|
||||
}
|
||||
|
||||
/** Checks if this role can manage an organization and all its teams */
|
||||
public boolean isOrgAdmin() {
|
||||
return isSystemAdmin() || this == ORG_ADMIN;
|
||||
}
|
||||
|
||||
/** Checks if this role can manage a specific team */
|
||||
public boolean isTeamLead() {
|
||||
return isOrgAdmin() || this == TEAM_LEAD;
|
||||
}
|
||||
|
||||
/** Gets the hierarchy level of this role (higher number = more permissions) */
|
||||
public int getHierarchyLevel() {
|
||||
return switch (this) {
|
||||
case SYSTEM_ADMIN, ADMIN -> 4;
|
||||
case ORG_ADMIN -> 3;
|
||||
case TEAM_LEAD -> 2;
|
||||
case USER -> 1;
|
||||
default -> 0; // Limited users
|
||||
};
|
||||
}
|
||||
|
||||
/** Checks if this role has higher or equal authority than another role */
|
||||
public boolean hasAuthorityOver(Role otherRole) {
|
||||
return this.getHierarchyLevel() >= otherRole.getHierarchyLevel();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
package stirling.software.proprietary.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "organizations")
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
@ToString(onlyExplicitlyIncluded = true)
|
||||
public class Organization implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "org_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "name", unique = true, nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@OneToMany(mappedBy = "organization", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<Team> teams = new HashSet<>();
|
||||
|
||||
public void addTeam(Team team) {
|
||||
teams.add(team);
|
||||
team.setOrganization(this);
|
||||
}
|
||||
|
||||
public void removeTeam(Team team) {
|
||||
teams.remove(team);
|
||||
team.setOrganization(null);
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ import lombok.*;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
|
||||
@Entity
|
||||
@Table(name = "teams")
|
||||
@Table(name = "teams", uniqueConstraints = @UniqueConstraint(columnNames = {"name", "org_id"}))
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@ -26,9 +26,15 @@ public class Team implements Serializable {
|
||||
@Column(name = "team_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "name", unique = true, nullable = false)
|
||||
@Column(name = "name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(
|
||||
name = "org_id",
|
||||
nullable = true) // Nullable for backward compatibility during migration
|
||||
private Organization organization;
|
||||
|
||||
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<User> users = new HashSet<>();
|
||||
|
||||
|
@ -0,0 +1,15 @@
|
||||
package stirling.software.proprietary.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OrganizationWithTeamCountDTO {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private Long teamCount;
|
||||
}
|
@ -15,9 +15,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import stirling.software.proprietary.security.service.DatabaseServiceInterface;
|
||||
import stirling.software.proprietary.security.service.OrganizationService;
|
||||
import stirling.software.proprietary.security.service.TeamService;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
@ -28,6 +31,8 @@ public class InitialSecuritySetup {
|
||||
|
||||
private final UserService userService;
|
||||
private final TeamService teamService;
|
||||
private final OrganizationService organizationService;
|
||||
private final TeamRepository teamRepository;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final DatabaseServiceInterface databaseService;
|
||||
|
||||
@ -44,6 +49,8 @@ public class InitialSecuritySetup {
|
||||
}
|
||||
|
||||
userService.migrateOauth2ToSSO();
|
||||
migrateAdminRolesToSystemAdmin();
|
||||
assignTeamsToDefaultOrganizationIfMissing();
|
||||
assignUsersToDefaultTeamIfMissing();
|
||||
initializeInternalApiUser();
|
||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||
@ -52,6 +59,49 @@ public class InitialSecuritySetup {
|
||||
}
|
||||
}
|
||||
|
||||
private void assignTeamsToDefaultOrganizationIfMissing() {
|
||||
// Find teams without organizations (legacy teams from before org feature)
|
||||
List<Team> teamsWithoutOrg = teamRepository.findTeamsWithoutOrganization();
|
||||
|
||||
if (teamsWithoutOrg.isEmpty()) {
|
||||
log.debug("No teams without organizations found - migration not needed");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Found {} teams without organizations. Starting migration...",
|
||||
teamsWithoutOrg.size());
|
||||
|
||||
// Ensure default organizations exist
|
||||
Organization defaultOrg = organizationService.getOrCreateDefaultOrganization();
|
||||
Organization internalOrg = organizationService.getOrCreateInternalOrganization();
|
||||
|
||||
int migratedCount = 0;
|
||||
for (Team team : teamsWithoutOrg) {
|
||||
try {
|
||||
// Assign internal team to internal org, all others to default org
|
||||
if (TeamService.INTERNAL_TEAM_NAME.equals(team.getName())) {
|
||||
team.setOrganization(internalOrg);
|
||||
log.debug("Assigned team '{}' to internal organization", team.getName());
|
||||
} else {
|
||||
team.setOrganization(defaultOrg);
|
||||
log.debug("Assigned team '{}' to default organization", team.getName());
|
||||
}
|
||||
teamRepository.save(team);
|
||||
migratedCount++;
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to migrate team '{}' to organization: {}",
|
||||
team.getName(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (migratedCount > 0) {
|
||||
log.info("Successfully migrated {} teams to organizations", migratedCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void assignUsersToDefaultTeamIfMissing() {
|
||||
Team defaultTeam = teamService.getOrCreateDefaultTeam();
|
||||
Team internalTeam = teamService.getOrCreateInternalTeam();
|
||||
@ -86,7 +136,7 @@ public class InitialSecuritySetup {
|
||||
|
||||
Team team = teamService.getOrCreateDefaultTeam();
|
||||
userService.saveUser(
|
||||
initialUsername, initialPassword, team, Role.ADMIN.getRoleId(), false);
|
||||
initialUsername, initialPassword, team, Role.SYSTEM_ADMIN.getRoleId(), false);
|
||||
log.info("Admin user created: {}", initialUsername);
|
||||
} else {
|
||||
createDefaultAdminUser();
|
||||
@ -100,7 +150,7 @@ public class InitialSecuritySetup {
|
||||
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
|
||||
Team team = teamService.getOrCreateDefaultTeam();
|
||||
userService.saveUser(
|
||||
defaultUsername, defaultPassword, team, Role.ADMIN.getRoleId(), true);
|
||||
defaultUsername, defaultPassword, team, Role.SYSTEM_ADMIN.getRoleId(), true);
|
||||
log.info("Default admin user created: {}", defaultUsername);
|
||||
}
|
||||
}
|
||||
@ -134,4 +184,32 @@ public class InitialSecuritySetup {
|
||||
}
|
||||
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
|
||||
}
|
||||
|
||||
private void migrateAdminRolesToSystemAdmin() {
|
||||
List<User> adminUsers = userService.findByRole(Role.ADMIN.getRoleId());
|
||||
|
||||
if (adminUsers.isEmpty()) {
|
||||
log.debug("No ROLE_ADMIN users found - migration not needed");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Found {} ROLE_ADMIN users. Converting to SYSTEM_ADMIN...", adminUsers.size());
|
||||
|
||||
int migratedCount = 0;
|
||||
for (User user : adminUsers) {
|
||||
try {
|
||||
user.setUserRole(Role.SYSTEM_ADMIN);
|
||||
userService.saveUser(user);
|
||||
log.debug("Converted user '{}' from ROLE_ADMIN to SYSTEM_ADMIN", user.getUsername());
|
||||
migratedCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to migrate user '{}' from ROLE_ADMIN to SYSTEM_ADMIN: {}",
|
||||
user.getUsername(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (migratedCount > 0) {
|
||||
log.info("Successfully migrated {} users from ROLE_ADMIN to SYSTEM_ADMIN", migratedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
@ -74,6 +75,7 @@ public class SecurityConfiguration {
|
||||
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||
private final Environment environment;
|
||||
|
||||
public SecurityConfiguration(
|
||||
PersistentLoginRepository persistentLoginRepository,
|
||||
@ -91,7 +93,8 @@ public class SecurityConfiguration {
|
||||
@Autowired(required = false)
|
||||
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||
@Autowired(required = false)
|
||||
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
|
||||
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
|
||||
Environment environment) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.userService = userService;
|
||||
this.loginEnabledValue = loginEnabledValue;
|
||||
@ -106,6 +109,7 @@ public class SecurityConfiguration {
|
||||
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
||||
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
||||
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ -129,33 +133,49 @@ public class SecurityConfiguration {
|
||||
new CsrfTokenRequestAttributeHandler();
|
||||
requestHandler.setCsrfRequestAttributeName(null);
|
||||
http.csrf(
|
||||
csrf ->
|
||||
csrf.ignoringRequestMatchers(
|
||||
request -> {
|
||||
String apiKey = request.getHeader("X-API-KEY");
|
||||
// If there's no API key, don't ignore CSRF
|
||||
csrf -> {
|
||||
var csrfConfig =
|
||||
csrf.ignoringRequestMatchers(
|
||||
request -> {
|
||||
String apiKey = request.getHeader("X-API-KEY");
|
||||
// If there's no API key, don't ignore CSRF
|
||||
// (return false)
|
||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// Validate API key using existing UserService
|
||||
try {
|
||||
Optional<User> user =
|
||||
userService.getUserByApiKey(apiKey);
|
||||
// If API key is valid, ignore CSRF (return
|
||||
// true)
|
||||
// If API key is invalid, don't ignore CSRF
|
||||
// (return false)
|
||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// Validate API key using existing UserService
|
||||
try {
|
||||
Optional<User> user =
|
||||
userService.getUserByApiKey(apiKey);
|
||||
// If API key is valid, ignore CSRF (return
|
||||
// true)
|
||||
// If API key is invalid, don't ignore CSRF
|
||||
// (return false)
|
||||
return user.isPresent();
|
||||
} catch (Exception e) {
|
||||
// If there's any error validating the API
|
||||
// key, don't ignore CSRF
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.csrfTokenRepository(cookieRepo)
|
||||
.csrfTokenRequestHandler(requestHandler));
|
||||
return user.isPresent();
|
||||
} catch (Exception e) {
|
||||
// If there's any error validating the API
|
||||
// key, don't ignore CSRF
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Only ignore CSRF for H2 console if H2 console is enabled
|
||||
if (isH2ConsoleEnabled()) {
|
||||
csrfConfig = csrfConfig.ignoringRequestMatchers("/h2-console/**");
|
||||
}
|
||||
|
||||
csrfConfig
|
||||
.csrfTokenRepository(cookieRepo)
|
||||
.csrfTokenRequestHandler(requestHandler);
|
||||
});
|
||||
}
|
||||
|
||||
// Allow H2 console frames only if H2 console is enabled
|
||||
if (isH2ConsoleEnabled()) {
|
||||
http.headers(
|
||||
headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()));
|
||||
}
|
||||
|
||||
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
http.sessionManagement(
|
||||
@ -215,6 +235,9 @@ public class SecurityConfiguration {
|
||||
|| trimmedUri.startsWith("/images/")
|
||||
|| trimmedUri.startsWith("/public/")
|
||||
|| trimmedUri.startsWith("/css/")
|
||||
|| (isH2ConsoleEnabled()
|
||||
&& trimmedUri.startsWith(
|
||||
"/h2-console/"))
|
||||
|| trimmedUri.startsWith("/fonts/")
|
||||
|| trimmedUri.startsWith("/js/")
|
||||
|| trimmedUri.startsWith(
|
||||
@ -323,4 +346,8 @@ public class SecurityConfiguration {
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
return new JPATokenRepositoryImpl(persistentLoginRepository);
|
||||
}
|
||||
|
||||
private boolean isH2ConsoleEnabled() {
|
||||
return environment.getProperty("spring.h2.console.enabled", Boolean.class, false);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,216 @@
|
||||
package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.config.PremiumEndpoint;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import stirling.software.proprietary.security.service.RoleBasedAuthorizationService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/org-admin")
|
||||
@Tag(name = "Organization Admin", description = "Organization Admin Management APIs")
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@PremiumEndpoint
|
||||
public class OrgAdminController {
|
||||
|
||||
private final TeamRepository teamRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final RoleBasedAuthorizationService authorizationService;
|
||||
|
||||
/** Get all teams in the org admin's organization */
|
||||
@GetMapping("/teams")
|
||||
public ResponseEntity<List<Team>> getOrganizationTeams() {
|
||||
if (!authorizationService.canManageOrgTeams()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
User currentUser = authorizationService.getCurrentUser();
|
||||
if (currentUser == null || currentUser.getOrganization() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
List<Team> teams =
|
||||
teamRepository.findByOrganizationId(currentUser.getOrganization().getId());
|
||||
return ResponseEntity.ok(teams);
|
||||
}
|
||||
|
||||
/** Get all users in the org admin's organization */
|
||||
@GetMapping("/users")
|
||||
public ResponseEntity<List<User>> getOrganizationUsers() {
|
||||
if (!authorizationService.canManageOrgUsers()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
User currentUser = authorizationService.getCurrentUser();
|
||||
if (currentUser == null || currentUser.getOrganization() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
// Get all users in teams belonging to this organization
|
||||
List<Team> orgTeams =
|
||||
teamRepository.findByOrganizationId(currentUser.getOrganization().getId());
|
||||
List<User> orgUsers =
|
||||
orgTeams.stream().flatMap(team -> team.getUsers().stream()).distinct().toList();
|
||||
|
||||
return ResponseEntity.ok(orgUsers);
|
||||
}
|
||||
|
||||
/** Assign a user to a team within the organization */
|
||||
@PostMapping("/assign-user-to-team")
|
||||
@Transactional
|
||||
public ResponseEntity<?> assignUserToTeam(
|
||||
@RequestParam("userId") Long userId, @RequestParam("teamId") Long teamId) {
|
||||
|
||||
if (!authorizationService.canManageOrgUsers()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to manage organization users");
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
Optional<Team> teamOpt = teamRepository.findById(teamId);
|
||||
|
||||
if (userOpt.isEmpty() || teamOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
Team team = teamOpt.get();
|
||||
|
||||
if (!authorizationService.canAddUserToTeam(userId, team)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to add user to this team");
|
||||
}
|
||||
|
||||
// Assign user to team
|
||||
user.setTeam(team);
|
||||
userRepository.save(user);
|
||||
|
||||
return ResponseEntity.ok().body("User assigned to team successfully");
|
||||
}
|
||||
|
||||
/** Promote a user to team lead */
|
||||
@PostMapping("/promote-to-team-lead")
|
||||
@Transactional
|
||||
public ResponseEntity<?> promoteToTeamLead(@RequestParam("userId") Long userId) {
|
||||
if (!authorizationService.canManageUser(userId)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to manage this user");
|
||||
}
|
||||
|
||||
if (!authorizationService.canAssignRole(Role.TEAM_LEAD)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to assign team lead role");
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
// User must be in a team to become a team lead
|
||||
if (user.getTeam() == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("User must be assigned to a team before becoming a team lead");
|
||||
}
|
||||
|
||||
user.setUserRole(Role.TEAM_LEAD);
|
||||
userRepository.save(user);
|
||||
|
||||
return ResponseEntity.ok().body("User promoted to team lead successfully");
|
||||
}
|
||||
|
||||
/** Demote a team lead to regular user */
|
||||
@PostMapping("/demote-from-team-lead")
|
||||
@Transactional
|
||||
public ResponseEntity<?> demoteFromTeamLead(@RequestParam("userId") Long userId) {
|
||||
if (!authorizationService.canManageUser(userId)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to manage this user");
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
user.setUserRole(Role.USER);
|
||||
userRepository.save(user);
|
||||
|
||||
return ResponseEntity.ok().body("User demoted from team lead successfully");
|
||||
}
|
||||
|
||||
/** Create a new team in the organization */
|
||||
@PostMapping("/create-team")
|
||||
@Transactional
|
||||
public ResponseEntity<?> createTeam(@RequestParam("teamName") String teamName) {
|
||||
if (!authorizationService.canManageOrgTeams()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to create teams");
|
||||
}
|
||||
|
||||
User currentUser = authorizationService.getCurrentUser();
|
||||
if (currentUser == null || currentUser.getOrganization() == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Org admin must be assigned to an organization");
|
||||
}
|
||||
|
||||
Organization organization = currentUser.getOrganization();
|
||||
|
||||
// Check if team name already exists in the organization
|
||||
if (teamRepository.existsByNameIgnoreCaseAndOrganizationId(
|
||||
teamName, organization.getId())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Team with name '" + teamName + "' already exists in this organization");
|
||||
}
|
||||
|
||||
Team newTeam = new Team();
|
||||
newTeam.setName(teamName);
|
||||
newTeam.setOrganization(organization);
|
||||
|
||||
Team savedTeam = teamRepository.save(newTeam);
|
||||
return ResponseEntity.ok(savedTeam);
|
||||
}
|
||||
|
||||
/** Remove a user from the organization (removes from their team) */
|
||||
@PostMapping("/remove-user")
|
||||
@Transactional
|
||||
public ResponseEntity<?> removeUserFromOrganization(@RequestParam("userId") Long userId) {
|
||||
if (!authorizationService.canRemoveUserFromTeam(userId)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to remove this user");
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
user.setTeam(null);
|
||||
user.setUserRole(Role.USER); // Reset to basic user role
|
||||
userRepository.save(user);
|
||||
|
||||
return ResponseEntity.ok().body("User removed from organization successfully");
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.model.dto.OrganizationWithTeamCountDTO;
|
||||
import stirling.software.proprietary.security.repository.OrganizationRepository;
|
||||
import stirling.software.proprietary.security.service.OrganizationService;
|
||||
import stirling.software.proprietary.security.service.RoleBasedAuthorizationService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/organizations")
|
||||
@RequiredArgsConstructor
|
||||
public class OrganizationController {
|
||||
|
||||
private final OrganizationRepository organizationRepository;
|
||||
private final OrganizationService organizationService;
|
||||
private final RoleBasedAuthorizationService authorizationService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<OrganizationWithTeamCountDTO>> getAllOrganizations() {
|
||||
if (!authorizationService.canManageOrganizations()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
List<OrganizationWithTeamCountDTO> organizations =
|
||||
organizationRepository.findAllOrganizationsWithTeamCount();
|
||||
return ResponseEntity.ok(organizations);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Organization> getOrganization(@PathVariable Long id) {
|
||||
Optional<Organization> organizationOpt = organizationRepository.findById(id);
|
||||
if (organizationOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Organization organization = organizationOpt.get();
|
||||
if (!authorizationService.canViewOrganization(organization)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(organization);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> createOrganization(@RequestBody Organization organization) {
|
||||
if (!authorizationService.canManageOrganizations()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to create organizations");
|
||||
}
|
||||
|
||||
if (organizationRepository.existsByNameIgnoreCase(organization.getName())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Organization with name '" + organization.getName() + "' already exists");
|
||||
}
|
||||
Organization savedOrganization = organizationRepository.save(organization);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(savedOrganization);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> updateOrganization(
|
||||
@PathVariable Long id, @RequestBody Organization organization) {
|
||||
if (!authorizationService.canManageOrganizations()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to update organizations");
|
||||
}
|
||||
|
||||
Optional<Organization> existingOrganization = organizationRepository.findById(id);
|
||||
if (existingOrganization.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (organizationRepository.existsByNameIgnoreCase(organization.getName())
|
||||
&& !existingOrganization.get().getName().equalsIgnoreCase(organization.getName())) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Organization with name '" + organization.getName() + "' already exists");
|
||||
}
|
||||
|
||||
organization.setId(id);
|
||||
Organization savedOrganization = organizationRepository.save(organization);
|
||||
return ResponseEntity.ok(savedOrganization);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteOrganization(@PathVariable Long id) {
|
||||
if (!authorizationService.canManageOrganizations()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to delete organizations");
|
||||
}
|
||||
|
||||
Optional<Organization> organization = organizationRepository.findById(id);
|
||||
if (organization.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// Prevent deletion of default organizations
|
||||
if (OrganizationService.DEFAULT_ORG_NAME.equals(organization.get().getName())
|
||||
|| OrganizationService.INTERNAL_ORG_NAME.equals(organization.get().getName())) {
|
||||
return ResponseEntity.badRequest().body("Cannot delete system organizations");
|
||||
}
|
||||
|
||||
if (!organization.get().getTeams().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Cannot delete organization with existing teams");
|
||||
}
|
||||
|
||||
organizationRepository.deleteById(id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
@ -14,11 +13,16 @@ import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.config.PremiumEndpoint;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.OrganizationRepository;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import stirling.software.proprietary.security.service.OrganizationService;
|
||||
import stirling.software.proprietary.security.service.OrganizationValidationService;
|
||||
import stirling.software.proprietary.security.service.RoleBasedAuthorizationService;
|
||||
import stirling.software.proprietary.security.service.TeamService;
|
||||
|
||||
@Controller
|
||||
@ -31,20 +35,33 @@ public class TeamController {
|
||||
|
||||
private final TeamRepository teamRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final OrganizationRepository organizationRepository;
|
||||
private final OrganizationService organizationService;
|
||||
private final OrganizationValidationService organizationValidationService;
|
||||
private final RoleBasedAuthorizationService authorizationService;
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PostMapping("/create")
|
||||
public RedirectView createTeam(@RequestParam("name") String name) {
|
||||
if (teamRepository.existsByNameIgnoreCase(name)) {
|
||||
public RedirectView createTeam(
|
||||
@RequestParam("name") String name,
|
||||
@RequestParam("organizationId") Long organizationId) {
|
||||
if (!authorizationService.canManageOrgTeams()) {
|
||||
return new RedirectView("/teams?messageType=accessDenied");
|
||||
}
|
||||
Organization organization = organizationService.getOrCreateDefaultOrganization();
|
||||
if (organizationId != null) {
|
||||
organization = organizationRepository.findById(organizationId).orElse(organization);
|
||||
}
|
||||
|
||||
if (teamRepository.existsByNameIgnoreCaseAndOrganizationId(name, organization.getId())) {
|
||||
return new RedirectView("/teams?messageType=teamExists");
|
||||
}
|
||||
Team team = new Team();
|
||||
team.setName(name);
|
||||
team.setOrganization(organization);
|
||||
teamRepository.save(team);
|
||||
return new RedirectView("/teams?messageType=teamCreated");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PostMapping("/rename")
|
||||
public RedirectView renameTeam(
|
||||
@RequestParam("teamId") Long teamId, @RequestParam("newName") String newName) {
|
||||
@ -52,10 +69,16 @@ public class TeamController {
|
||||
if (existing.isEmpty()) {
|
||||
return new RedirectView("/teams?messageType=teamNotFound");
|
||||
}
|
||||
if (teamRepository.existsByNameIgnoreCase(newName)) {
|
||||
Team team = existing.get();
|
||||
|
||||
if (!authorizationService.canManageTeam(team)) {
|
||||
return new RedirectView("/teams?messageType=accessDenied");
|
||||
}
|
||||
|
||||
if (teamRepository.existsByNameIgnoreCaseAndOrganizationId(
|
||||
newName, team.getOrganization().getId())) {
|
||||
return new RedirectView("/teams?messageType=teamNameExists");
|
||||
}
|
||||
Team team = existing.get();
|
||||
|
||||
// Prevent renaming the Internal team
|
||||
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||
@ -67,7 +90,6 @@ public class TeamController {
|
||||
return new RedirectView("/teams?messageType=teamRenamed");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PostMapping("/delete")
|
||||
@Transactional
|
||||
public RedirectView deleteTeam(@RequestParam("teamId") Long teamId) {
|
||||
@ -78,6 +100,10 @@ public class TeamController {
|
||||
|
||||
Team team = teamOpt.get();
|
||||
|
||||
if (!authorizationService.canManageTeam(team)) {
|
||||
return new RedirectView("/teams?messageType=accessDenied");
|
||||
}
|
||||
|
||||
// Prevent deleting the Internal team
|
||||
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||
return new RedirectView("/teams?messageType=internalTeamNotAccessible");
|
||||
@ -92,7 +118,6 @@ public class TeamController {
|
||||
return new RedirectView("/teams?messageType=teamDeleted");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PostMapping("/addUser")
|
||||
@Transactional
|
||||
public RedirectView addUserToTeam(
|
||||
@ -104,6 +129,10 @@ public class TeamController {
|
||||
.findById(teamId)
|
||||
.orElseThrow(() -> new RuntimeException("Team not found"));
|
||||
|
||||
if (!authorizationService.canAddUserToTeam(userId, team)) {
|
||||
return new RedirectView("/teams/" + teamId + "?error=accessDenied");
|
||||
}
|
||||
|
||||
// Prevent adding users to the Internal team
|
||||
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||
return new RedirectView("/teams?error=internalTeamNotAccessible");
|
||||
@ -121,6 +150,13 @@ public class TeamController {
|
||||
return new RedirectView("/teams/" + teamId + "?error=cannotMoveInternalUsers");
|
||||
}
|
||||
|
||||
// Ensure user and team are in the same organization (or user has no org yet)
|
||||
if (user.getOrganization() != null
|
||||
&& !organizationValidationService.isTeamInOrganization(
|
||||
team, user.getOrganization())) {
|
||||
return new RedirectView("/teams/" + teamId + "?error=userNotInSameOrganization");
|
||||
}
|
||||
|
||||
// Assign user to team
|
||||
user.setTeam(team);
|
||||
userRepository.save(user);
|
||||
|
@ -0,0 +1,187 @@
|
||||
package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.config.PremiumEndpoint;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import stirling.software.proprietary.security.service.RoleBasedAuthorizationService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/team-lead")
|
||||
@Tag(name = "Team Lead", description = "Team Lead Management APIs")
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@PremiumEndpoint
|
||||
public class TeamLeadController {
|
||||
|
||||
private final TeamRepository teamRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final RoleBasedAuthorizationService authorizationService;
|
||||
|
||||
/** Get team members that the current team lead can manage */
|
||||
@GetMapping("/my-team-members")
|
||||
public ResponseEntity<List<User>> getMyTeamMembers() {
|
||||
if (!authorizationService.canManageTeamUsers()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
User currentUser = authorizationService.getCurrentUser();
|
||||
if (currentUser == null || currentUser.getTeam() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
List<User> teamMembers = userRepository.findByTeam(currentUser.getTeam());
|
||||
return ResponseEntity.ok(teamMembers);
|
||||
}
|
||||
|
||||
/** Add a user to the team lead's team */
|
||||
@PostMapping("/add-member")
|
||||
@Transactional
|
||||
public ResponseEntity<?> addMemberToMyTeam(@RequestParam("userId") Long userId) {
|
||||
User currentUser = authorizationService.getCurrentUser();
|
||||
if (currentUser == null || currentUser.getTeam() == null) {
|
||||
return ResponseEntity.badRequest().body("Team lead must be assigned to a team");
|
||||
}
|
||||
|
||||
if (!authorizationService.canAddUserToTeam(userId, currentUser.getTeam())) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to add users to this team");
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
|
||||
// Check if user is already in a team
|
||||
if (user.getTeam() != null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("User is already assigned to team: " + user.getTeam().getName());
|
||||
}
|
||||
|
||||
// Assign user to team
|
||||
user.setTeam(currentUser.getTeam());
|
||||
userRepository.save(user);
|
||||
|
||||
return ResponseEntity.ok().body("User added to team successfully");
|
||||
}
|
||||
|
||||
/** Remove a user from the team lead's team */
|
||||
@PostMapping("/remove-member")
|
||||
@Transactional
|
||||
public ResponseEntity<?> removeMemberFromMyTeam(@RequestParam("userId") Long userId) {
|
||||
if (!authorizationService.canRemoveUserFromTeam(userId)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to remove this user");
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
User currentUser = authorizationService.getCurrentUser();
|
||||
|
||||
// Prevent team leads from removing themselves
|
||||
if (currentUser != null && currentUser.getId().equals(userId)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Team leads cannot remove themselves from the team");
|
||||
}
|
||||
|
||||
// Remove user from team
|
||||
user.setTeam(null);
|
||||
userRepository.save(user);
|
||||
|
||||
return ResponseEntity.ok().body("User removed from team successfully");
|
||||
}
|
||||
|
||||
/** Get users that can be added to the team (within same organization, not in any team) */
|
||||
@GetMapping("/available-users")
|
||||
public ResponseEntity<List<User>> getAvailableUsers() {
|
||||
if (!authorizationService.canManageTeamUsers()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
User currentUser = authorizationService.getCurrentUser();
|
||||
if (currentUser == null || currentUser.getOrganization() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
// Find users in the same organization who are not in any team
|
||||
List<User> availableUsers =
|
||||
userRepository.findUsersInOrganizationWithoutTeam(
|
||||
currentUser.getOrganization().getId());
|
||||
|
||||
return ResponseEntity.ok(availableUsers);
|
||||
}
|
||||
|
||||
/** Update a team member's role (team leads can only assign USER role) */
|
||||
@PostMapping("/update-member-role")
|
||||
@Transactional
|
||||
public ResponseEntity<?> updateMemberRole(
|
||||
@RequestParam("userId") Long userId, @RequestParam("role") String roleString) {
|
||||
|
||||
if (!authorizationService.canManageUser(userId)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body("Not authorized to manage this user");
|
||||
}
|
||||
|
||||
try {
|
||||
Role newRole = Role.fromString(roleString);
|
||||
|
||||
// Team leads can only assign USER role
|
||||
if (!authorizationService.canAssignRole(newRole)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Not authorized to assign role: " + newRole.getRoleName());
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userRepository.findById(userId);
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
user.setUserRole(newRole);
|
||||
userRepository.save(user);
|
||||
|
||||
return ResponseEntity.ok().body("User role updated successfully");
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body("Invalid role: " + roleString);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get team information for the current team lead */
|
||||
@GetMapping("/my-team")
|
||||
public ResponseEntity<Team> getMyTeam() {
|
||||
User currentUser = authorizationService.getCurrentUser();
|
||||
if (currentUser == null || currentUser.getTeam() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
if (!authorizationService.canManageTeam(currentUser.getTeam())) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(currentUser.getTeam());
|
||||
}
|
||||
}
|
@ -36,5 +36,13 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
long countByTeam(Team team);
|
||||
|
||||
List<User> findByTeam(Team team);
|
||||
|
||||
List<User> findAllByTeam(Team team);
|
||||
|
||||
@Query("SELECT u FROM User u WHERE u.team IS NULL")
|
||||
List<User> findUsersInOrganizationWithoutTeam(@Param("organizationId") Long organizationId);
|
||||
|
||||
@Query("SELECT u FROM User u JOIN u.authorities a WHERE a.authority = :role")
|
||||
List<User> findByRole(@Param("role") String role);
|
||||
}
|
||||
|
@ -102,4 +102,88 @@ public class User implements Serializable {
|
||||
public boolean hasPassword() {
|
||||
return this.password != null && !this.password.isEmpty();
|
||||
}
|
||||
|
||||
public stirling.software.proprietary.model.Organization getOrganization() {
|
||||
return this.team != null ? this.team.getOrganization() : null;
|
||||
}
|
||||
|
||||
// Role-based permission methods
|
||||
public Role getUserRole() {
|
||||
String roleString = getRolesAsString();
|
||||
if (roleString == null || roleString.isEmpty()) return Role.USER;
|
||||
|
||||
try {
|
||||
return Role.fromString(roleString);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Role.USER; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSystemAdmin() {
|
||||
Role role = getUserRole();
|
||||
return role.isSystemAdmin();
|
||||
}
|
||||
|
||||
public boolean isOrgAdmin() {
|
||||
Role role = getUserRole();
|
||||
return role.isOrgAdmin();
|
||||
}
|
||||
|
||||
public boolean isTeamLead() {
|
||||
Role role = getUserRole();
|
||||
return role.isTeamLead();
|
||||
}
|
||||
|
||||
public boolean canManageUser(User otherUser) {
|
||||
// System admins can manage anyone
|
||||
if (isSystemAdmin()) return true;
|
||||
|
||||
// Org admins can manage users in their organization
|
||||
if (isOrgAdmin()) {
|
||||
stirling.software.proprietary.model.Organization thisOrg = getOrganization();
|
||||
stirling.software.proprietary.model.Organization otherOrg = otherUser.getOrganization();
|
||||
return thisOrg != null && otherOrg != null && thisOrg.getId().equals(otherOrg.getId());
|
||||
}
|
||||
|
||||
// Team leads can manage users in their team
|
||||
if (isTeamLead()) {
|
||||
return this.team != null
|
||||
&& otherUser.team != null
|
||||
&& this.team.getId().equals(otherUser.team.getId());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canManageTeam(stirling.software.proprietary.model.Team targetTeam) {
|
||||
if (targetTeam == null) return false;
|
||||
|
||||
// System admins can manage any team
|
||||
if (isSystemAdmin()) return true;
|
||||
|
||||
// Org admins can manage teams in their organization
|
||||
if (isOrgAdmin()) {
|
||||
stirling.software.proprietary.model.Organization thisOrg = getOrganization();
|
||||
stirling.software.proprietary.model.Organization teamOrg = targetTeam.getOrganization();
|
||||
return thisOrg != null && teamOrg != null && thisOrg.getId().equals(teamOrg.getId());
|
||||
}
|
||||
|
||||
// Team leads can only manage their own team
|
||||
if (isTeamLead()) {
|
||||
return this.team != null && this.team.getId().equals(targetTeam.getId());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setUserRole(Role role) {
|
||||
// Clear existing authorities
|
||||
this.authorities.clear();
|
||||
|
||||
// Add new authority
|
||||
Authority authority = new Authority();
|
||||
authority.setAuthority(role.getRoleId());
|
||||
authority.setUser(this);
|
||||
this.authorities.add(authority);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
package stirling.software.proprietary.security.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.model.dto.OrganizationWithTeamCountDTO;
|
||||
|
||||
@Repository
|
||||
public interface OrganizationRepository extends JpaRepository<Organization, Long> {
|
||||
Optional<Organization> findByName(String name);
|
||||
|
||||
@Query(
|
||||
"SELECT new stirling.software.proprietary.model.dto.OrganizationWithTeamCountDTO(o.id, o.name, o.description, COUNT(t)) "
|
||||
+ "FROM Organization o LEFT JOIN o.teams t GROUP BY o.id, o.name, o.description")
|
||||
List<OrganizationWithTeamCountDTO> findAllOrganizationsWithTeamCount();
|
||||
|
||||
boolean existsByNameIgnoreCase(String name);
|
||||
}
|
@ -14,10 +14,24 @@ import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
|
||||
public interface TeamRepository extends JpaRepository<Team, Long> {
|
||||
Optional<Team> findByName(String name);
|
||||
|
||||
Optional<Team> findByNameAndOrganizationId(String name, Long organizationId);
|
||||
|
||||
List<Team> findByOrganizationId(Long organizationId);
|
||||
|
||||
@Query(
|
||||
"SELECT new stirling.software.proprietary.model.dto.TeamWithUserCountDTO(t.id, t.name, COUNT(u)) "
|
||||
+ "FROM Team t LEFT JOIN t.users u WHERE t.organization.id = :organizationId GROUP BY t.id, t.name")
|
||||
List<TeamWithUserCountDTO> findAllTeamsWithUserCountByOrganizationId(Long organizationId);
|
||||
|
||||
@Query(
|
||||
"SELECT new stirling.software.proprietary.model.dto.TeamWithUserCountDTO(t.id, t.name, COUNT(u)) "
|
||||
+ "FROM Team t LEFT JOIN t.users u GROUP BY t.id, t.name")
|
||||
List<TeamWithUserCountDTO> findAllTeamsWithUserCount();
|
||||
|
||||
boolean existsByNameIgnoreCase(String name);
|
||||
|
||||
boolean existsByNameIgnoreCaseAndOrganizationId(String name, Long organizationId);
|
||||
|
||||
@Query("SELECT t FROM Team t WHERE t.organization IS NULL")
|
||||
List<Team> findTeamsWithoutOrganization();
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.security.repository.OrganizationRepository;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OrganizationService {
|
||||
|
||||
private final OrganizationRepository organizationRepository;
|
||||
|
||||
public static final String DEFAULT_ORG_NAME = "Default Organization";
|
||||
public static final String INTERNAL_ORG_NAME = "Internal Organization";
|
||||
|
||||
public Organization getOrCreateDefaultOrganization() {
|
||||
return organizationRepository
|
||||
.findByName(DEFAULT_ORG_NAME)
|
||||
.orElseGet(
|
||||
() -> {
|
||||
Organization defaultOrg = new Organization();
|
||||
defaultOrg.setName(DEFAULT_ORG_NAME);
|
||||
defaultOrg.setDescription("Default organization for initial setup");
|
||||
return organizationRepository.save(defaultOrg);
|
||||
});
|
||||
}
|
||||
|
||||
public Organization getOrCreateInternalOrganization() {
|
||||
return organizationRepository
|
||||
.findByName(INTERNAL_ORG_NAME)
|
||||
.orElseGet(
|
||||
() -> {
|
||||
Organization internalOrg = new Organization();
|
||||
internalOrg.setName(INTERNAL_ORG_NAME);
|
||||
internalOrg.setDescription(
|
||||
"Internal organization for system operations");
|
||||
return organizationRepository.save(internalOrg);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OrganizationValidationService {
|
||||
|
||||
private final TeamRepository teamRepository;
|
||||
|
||||
/**
|
||||
* Validates that a user has access to a specific team. Users can only access teams within their
|
||||
* own organization.
|
||||
*/
|
||||
public boolean canUserAccessTeam(User user, Team team) {
|
||||
if (user == null || team == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Organization userOrg = user.getOrganization();
|
||||
Organization teamOrg = team.getOrganization();
|
||||
|
||||
if (userOrg == null || teamOrg == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return userOrg.getId().equals(teamOrg.getId());
|
||||
}
|
||||
|
||||
/** Validates that a user has access to a specific team by ID. */
|
||||
public boolean canUserAccessTeam(User user, Long teamId) {
|
||||
if (user == null || teamId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Organization userOrg = user.getOrganization();
|
||||
if (userOrg == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return teamRepository
|
||||
.findById(teamId)
|
||||
.map(team -> userOrg.getId().equals(team.getOrganization().getId()))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/** Validates that two users belong to the same organization. */
|
||||
public boolean areUsersInSameOrganization(User user1, User user2) {
|
||||
if (user1 == null || user2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Organization org1 = user1.getOrganization();
|
||||
Organization org2 = user2.getOrganization();
|
||||
|
||||
if (org1 == null || org2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return org1.getId().equals(org2.getId());
|
||||
}
|
||||
|
||||
/** Validates that a team belongs to a specific organization. */
|
||||
public boolean isTeamInOrganization(Team team, Organization organization) {
|
||||
if (team == null || organization == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Organization teamOrg = team.getOrganization();
|
||||
return teamOrg != null && teamOrg.getId().equals(organization.getId());
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RoleBasedAuthorizationService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/** Gets the current authenticated user */
|
||||
public User getCurrentUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String username = authentication.getName();
|
||||
return userRepository.findByUsername(username).orElse(null);
|
||||
}
|
||||
|
||||
/** Checks if current user can manage users across all organizations (System Admin) */
|
||||
public boolean canManageAllUsers() {
|
||||
User currentUser = getCurrentUser();
|
||||
return currentUser != null && currentUser.isSystemAdmin();
|
||||
}
|
||||
|
||||
/** Checks if current user can manage users within their organization (Org Admin or above) */
|
||||
public boolean canManageOrgUsers() {
|
||||
User currentUser = getCurrentUser();
|
||||
return currentUser != null && currentUser.isOrgAdmin();
|
||||
}
|
||||
|
||||
/** Checks if current user can manage team members (Team Lead or above) */
|
||||
public boolean canManageTeamUsers() {
|
||||
User currentUser = getCurrentUser();
|
||||
return currentUser != null && currentUser.isTeamLead();
|
||||
}
|
||||
|
||||
/** Checks if current user can manage a specific user */
|
||||
public boolean canManageUser(Long userId) {
|
||||
User currentUser = getCurrentUser();
|
||||
if (currentUser == null) return false;
|
||||
|
||||
User targetUser = userRepository.findById(userId).orElse(null);
|
||||
if (targetUser == null) return false;
|
||||
|
||||
return currentUser.canManageUser(targetUser);
|
||||
}
|
||||
|
||||
/** Checks if current user can manage a specific team */
|
||||
public boolean canManageTeam(Team team) {
|
||||
User currentUser = getCurrentUser();
|
||||
if (currentUser == null || team == null) return false;
|
||||
|
||||
return currentUser.canManageTeam(team);
|
||||
}
|
||||
|
||||
/** Checks if current user can manage teams within their organization */
|
||||
public boolean canManageOrgTeams() {
|
||||
User currentUser = getCurrentUser();
|
||||
return currentUser != null && currentUser.isOrgAdmin();
|
||||
}
|
||||
|
||||
/** Checks if current user can create/manage organizations (System Admin only) */
|
||||
public boolean canManageOrganizations() {
|
||||
return canManageAllUsers();
|
||||
}
|
||||
|
||||
/** Checks if current user can assign roles */
|
||||
public boolean canAssignRole(Role targetRole) {
|
||||
User currentUser = getCurrentUser();
|
||||
if (currentUser == null) return false;
|
||||
|
||||
// Users can only assign roles that are lower than or equal to their own
|
||||
return currentUser.getUserRole().hasAuthorityOver(targetRole);
|
||||
}
|
||||
|
||||
/** Checks if current user can remove a user from their team/organization */
|
||||
public boolean canRemoveUserFromTeam(Long userId) {
|
||||
return canManageUser(userId);
|
||||
}
|
||||
|
||||
/** Checks if current user can add a user to a specific team */
|
||||
public boolean canAddUserToTeam(Long userId, Team team) {
|
||||
User currentUser = getCurrentUser();
|
||||
if (currentUser == null || team == null) return false;
|
||||
|
||||
// Must be able to manage both the user and the target team
|
||||
return canManageUser(userId) && currentUser.canManageTeam(team);
|
||||
}
|
||||
|
||||
/** Gets the highest role the current user can assign to others */
|
||||
public Role getMaxAssignableRole() {
|
||||
User currentUser = getCurrentUser();
|
||||
if (currentUser == null) return Role.USER;
|
||||
|
||||
return switch (currentUser.getUserRole()) {
|
||||
case SYSTEM_ADMIN, ADMIN -> Role.ORG_ADMIN; // System admins can create org admins
|
||||
case ORG_ADMIN -> Role.TEAM_LEAD; // Org admins can create team leads
|
||||
case TEAM_LEAD -> Role.USER; // Team leads can only create regular users
|
||||
default -> Role.USER;
|
||||
};
|
||||
}
|
||||
|
||||
/** Checks if current user can view organization details */
|
||||
public boolean canViewOrganization(Organization organization) {
|
||||
User currentUser = getCurrentUser();
|
||||
if (currentUser == null || organization == null) return false;
|
||||
|
||||
// System admins can view any org
|
||||
if (currentUser.isSystemAdmin()) return true;
|
||||
|
||||
// Org admins and team leads can view their own organization
|
||||
Organization userOrg = currentUser.getOrganization();
|
||||
return userOrg != null && userOrg.getId().equals(organization.getId());
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
|
||||
@ -12,29 +13,46 @@ import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
public class TeamService {
|
||||
|
||||
private final TeamRepository teamRepository;
|
||||
private final OrganizationService organizationService;
|
||||
|
||||
public static final String DEFAULT_TEAM_NAME = "Default";
|
||||
public static final String INTERNAL_TEAM_NAME = "Internal";
|
||||
|
||||
public Team getOrCreateDefaultTeam() {
|
||||
Organization defaultOrg = organizationService.getOrCreateDefaultOrganization();
|
||||
return teamRepository
|
||||
.findByName(DEFAULT_TEAM_NAME)
|
||||
.findByNameAndOrganizationId(DEFAULT_TEAM_NAME, defaultOrg.getId())
|
||||
.orElseGet(
|
||||
() -> {
|
||||
Team defaultTeam = new Team();
|
||||
defaultTeam.setName(DEFAULT_TEAM_NAME);
|
||||
defaultTeam.setOrganization(defaultOrg);
|
||||
return teamRepository.save(defaultTeam);
|
||||
});
|
||||
}
|
||||
|
||||
public Team getOrCreateInternalTeam() {
|
||||
Organization internalOrg = organizationService.getOrCreateInternalOrganization();
|
||||
return teamRepository
|
||||
.findByName(INTERNAL_TEAM_NAME)
|
||||
.findByNameAndOrganizationId(INTERNAL_TEAM_NAME, internalOrg.getId())
|
||||
.orElseGet(
|
||||
() -> {
|
||||
Team internalTeam = new Team();
|
||||
internalTeam.setName(INTERNAL_TEAM_NAME);
|
||||
internalTeam.setOrganization(internalOrg);
|
||||
return teamRepository.save(internalTeam);
|
||||
});
|
||||
}
|
||||
|
||||
public Team getOrCreateTeamForOrganization(String teamName, Organization organization) {
|
||||
return teamRepository
|
||||
.findByNameAndOrganizationId(teamName, organization.getId())
|
||||
.orElseGet(
|
||||
() -> {
|
||||
Team team = new Team();
|
||||
team.setName(teamName);
|
||||
team.setOrganization(organization);
|
||||
return teamRepository.save(team);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -328,6 +328,10 @@ public class UserService implements UserServiceInterface {
|
||||
return userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
||||
}
|
||||
|
||||
public List<User> findByRole(String role) {
|
||||
return userRepository.findByRole(role);
|
||||
}
|
||||
|
||||
public Authority findRole(User user) {
|
||||
return authorityRepository.findByUserId(user.getId());
|
||||
}
|
||||
@ -626,4 +630,8 @@ public class UserService implements UserServiceInterface {
|
||||
public void saveAll(List<User> users) {
|
||||
userRepository.saveAll(users);
|
||||
}
|
||||
|
||||
public User saveUser(User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,13 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import stirling.software.proprietary.model.Organization;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@ -18,15 +21,24 @@ class TeamServiceTest {
|
||||
@Mock
|
||||
private TeamRepository teamRepository;
|
||||
|
||||
@Mock
|
||||
private OrganizationService organizationService;
|
||||
|
||||
@InjectMocks
|
||||
private TeamService teamService;
|
||||
|
||||
@Test
|
||||
void getDefaultTeam() {
|
||||
var organization = new Organization();
|
||||
organization.setId(1L);
|
||||
organization.setName("Default Organization");
|
||||
|
||||
var team = new Team();
|
||||
team.setName("Marleyans");
|
||||
team.setOrganization(organization);
|
||||
|
||||
when(teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME))
|
||||
when(organizationService.getOrCreateDefaultOrganization()).thenReturn(organization);
|
||||
when(teamRepository.findByNameAndOrganizationId(TeamService.DEFAULT_TEAM_NAME, organization.getId()))
|
||||
.thenReturn(Optional.of(team));
|
||||
|
||||
Team result = teamService.getOrCreateDefaultTeam();
|
||||
@ -36,12 +48,18 @@ class TeamServiceTest {
|
||||
|
||||
@Test
|
||||
void createDefaultTeam_whenRepositoryIsEmpty() {
|
||||
var organization = new Organization();
|
||||
organization.setId(1L);
|
||||
organization.setName("Default Organization");
|
||||
|
||||
String teamName = "Default";
|
||||
var defaultTeam = new Team();
|
||||
defaultTeam.setId(1L);
|
||||
defaultTeam.setName(teamName);
|
||||
defaultTeam.setOrganization(organization);
|
||||
|
||||
when(teamRepository.findByName(teamName))
|
||||
when(organizationService.getOrCreateDefaultOrganization()).thenReturn(organization);
|
||||
when(teamRepository.findByNameAndOrganizationId(teamName, organization.getId()))
|
||||
.thenReturn(Optional.empty());
|
||||
when(teamRepository.save(any(Team.class))).thenReturn(defaultTeam);
|
||||
|
||||
@ -52,10 +70,16 @@ class TeamServiceTest {
|
||||
|
||||
@Test
|
||||
void getInternalTeam() {
|
||||
var organization = new Organization();
|
||||
organization.setId(2L);
|
||||
organization.setName("Internal Organization");
|
||||
|
||||
var team = new Team();
|
||||
team.setName("Eldians");
|
||||
team.setOrganization(organization);
|
||||
|
||||
when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME))
|
||||
when(organizationService.getOrCreateInternalOrganization()).thenReturn(organization);
|
||||
when(teamRepository.findByNameAndOrganizationId(TeamService.INTERNAL_TEAM_NAME, organization.getId()))
|
||||
.thenReturn(Optional.of(team));
|
||||
|
||||
Team result = teamService.getOrCreateInternalTeam();
|
||||
@ -65,16 +89,20 @@ class TeamServiceTest {
|
||||
|
||||
@Test
|
||||
void createInternalTeam_whenRepositoryIsEmpty() {
|
||||
var organization = new Organization();
|
||||
organization.setId(2L);
|
||||
organization.setName("Internal Organization");
|
||||
|
||||
String teamName = "Internal";
|
||||
Team internalTeam = new Team();
|
||||
internalTeam.setId(2L);
|
||||
internalTeam.setName(teamName);
|
||||
internalTeam.setOrganization(organization);
|
||||
|
||||
when(teamRepository.findByName(teamName))
|
||||
when(organizationService.getOrCreateInternalOrganization()).thenReturn(organization);
|
||||
when(teamRepository.findByNameAndOrganizationId(TeamService.INTERNAL_TEAM_NAME, organization.getId()))
|
||||
.thenReturn(Optional.empty());
|
||||
when(teamRepository.save(any(Team.class))).thenReturn(internalTeam);
|
||||
when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
Team result = teamService.getOrCreateInternalTeam();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user