This commit is contained in:
Anthony Stirling 2025-07-31 12:16:01 +01:00
parent 0966b919cb
commit 5e5f2dda83
20 changed files with 1253 additions and 49 deletions

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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<>();

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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);
});
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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);
});
}
}

View File

@ -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);
}
}

View File

@ -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();