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
|
@RequiredArgsConstructor
|
||||||
public enum Role {
|
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"),
|
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"),
|
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.user"),
|
||||||
|
|
||||||
// 40 API calls Per Day, 40 web calls
|
// 40 API calls Per Day, 40 web calls
|
||||||
@ -63,4 +76,35 @@ public enum Role {
|
|||||||
}
|
}
|
||||||
throw new IllegalArgumentException("No Role defined for id: " + roleId);
|
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;
|
import stirling.software.proprietary.security.model.User;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "teams")
|
@Table(name = "teams", uniqueConstraints = @UniqueConstraint(columnNames = {"name", "org_id"}))
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@ -26,9 +26,15 @@ public class Team implements Serializable {
|
|||||||
@Column(name = "team_id")
|
@Column(name = "team_id")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column(name = "name", unique = true, nullable = false)
|
@Column(name = "name", nullable = false)
|
||||||
private String name;
|
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)
|
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private Set<User> users = new HashSet<>();
|
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.ApplicationProperties;
|
||||||
import stirling.software.common.model.enumeration.Role;
|
import stirling.software.common.model.enumeration.Role;
|
||||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
|
import stirling.software.proprietary.model.Organization;
|
||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
import stirling.software.proprietary.security.model.User;
|
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.DatabaseServiceInterface;
|
||||||
|
import stirling.software.proprietary.security.service.OrganizationService;
|
||||||
import stirling.software.proprietary.security.service.TeamService;
|
import stirling.software.proprietary.security.service.TeamService;
|
||||||
import stirling.software.proprietary.security.service.UserService;
|
import stirling.software.proprietary.security.service.UserService;
|
||||||
|
|
||||||
@ -28,6 +31,8 @@ public class InitialSecuritySetup {
|
|||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final TeamService teamService;
|
private final TeamService teamService;
|
||||||
|
private final OrganizationService organizationService;
|
||||||
|
private final TeamRepository teamRepository;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final DatabaseServiceInterface databaseService;
|
private final DatabaseServiceInterface databaseService;
|
||||||
|
|
||||||
@ -44,6 +49,8 @@ public class InitialSecuritySetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userService.migrateOauth2ToSSO();
|
userService.migrateOauth2ToSSO();
|
||||||
|
migrateAdminRolesToSystemAdmin();
|
||||||
|
assignTeamsToDefaultOrganizationIfMissing();
|
||||||
assignUsersToDefaultTeamIfMissing();
|
assignUsersToDefaultTeamIfMissing();
|
||||||
initializeInternalApiUser();
|
initializeInternalApiUser();
|
||||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
} 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() {
|
private void assignUsersToDefaultTeamIfMissing() {
|
||||||
Team defaultTeam = teamService.getOrCreateDefaultTeam();
|
Team defaultTeam = teamService.getOrCreateDefaultTeam();
|
||||||
Team internalTeam = teamService.getOrCreateInternalTeam();
|
Team internalTeam = teamService.getOrCreateInternalTeam();
|
||||||
@ -86,7 +136,7 @@ public class InitialSecuritySetup {
|
|||||||
|
|
||||||
Team team = teamService.getOrCreateDefaultTeam();
|
Team team = teamService.getOrCreateDefaultTeam();
|
||||||
userService.saveUser(
|
userService.saveUser(
|
||||||
initialUsername, initialPassword, team, Role.ADMIN.getRoleId(), false);
|
initialUsername, initialPassword, team, Role.SYSTEM_ADMIN.getRoleId(), false);
|
||||||
log.info("Admin user created: {}", initialUsername);
|
log.info("Admin user created: {}", initialUsername);
|
||||||
} else {
|
} else {
|
||||||
createDefaultAdminUser();
|
createDefaultAdminUser();
|
||||||
@ -100,7 +150,7 @@ public class InitialSecuritySetup {
|
|||||||
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
|
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
|
||||||
Team team = teamService.getOrCreateDefaultTeam();
|
Team team = teamService.getOrCreateDefaultTeam();
|
||||||
userService.saveUser(
|
userService.saveUser(
|
||||||
defaultUsername, defaultPassword, team, Role.ADMIN.getRoleId(), true);
|
defaultUsername, defaultPassword, team, Role.SYSTEM_ADMIN.getRoleId(), true);
|
||||||
log.info("Default admin user created: {}", defaultUsername);
|
log.info("Default admin user created: {}", defaultUsername);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,4 +184,32 @@ public class InitialSecuritySetup {
|
|||||||
}
|
}
|
||||||
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
|
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.Configuration;
|
||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.security.authentication.ProviderManager;
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
@ -74,6 +75,7 @@ public class SecurityConfiguration {
|
|||||||
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||||
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||||
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||||
|
private final Environment environment;
|
||||||
|
|
||||||
public SecurityConfiguration(
|
public SecurityConfiguration(
|
||||||
PersistentLoginRepository persistentLoginRepository,
|
PersistentLoginRepository persistentLoginRepository,
|
||||||
@ -91,7 +93,8 @@ public class SecurityConfiguration {
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
|
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
|
||||||
|
Environment environment) {
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.loginEnabledValue = loginEnabledValue;
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
@ -106,6 +109,7 @@ public class SecurityConfiguration {
|
|||||||
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
||||||
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
||||||
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
||||||
|
this.environment = environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ -129,33 +133,49 @@ public class SecurityConfiguration {
|
|||||||
new CsrfTokenRequestAttributeHandler();
|
new CsrfTokenRequestAttributeHandler();
|
||||||
requestHandler.setCsrfRequestAttributeName(null);
|
requestHandler.setCsrfRequestAttributeName(null);
|
||||||
http.csrf(
|
http.csrf(
|
||||||
csrf ->
|
csrf -> {
|
||||||
csrf.ignoringRequestMatchers(
|
var csrfConfig =
|
||||||
request -> {
|
csrf.ignoringRequestMatchers(
|
||||||
String apiKey = request.getHeader("X-API-KEY");
|
request -> {
|
||||||
// If there's no API key, don't ignore CSRF
|
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)
|
// (return false)
|
||||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
return user.isPresent();
|
||||||
return false;
|
} catch (Exception e) {
|
||||||
}
|
// If there's any error validating the API
|
||||||
// Validate API key using existing UserService
|
// key, don't ignore CSRF
|
||||||
try {
|
return false;
|
||||||
Optional<User> user =
|
}
|
||||||
userService.getUserByApiKey(apiKey);
|
});
|
||||||
// If API key is valid, ignore CSRF (return
|
|
||||||
// true)
|
// Only ignore CSRF for H2 console if H2 console is enabled
|
||||||
// If API key is invalid, don't ignore CSRF
|
if (isH2ConsoleEnabled()) {
|
||||||
// (return false)
|
csrfConfig = csrfConfig.ignoringRequestMatchers("/h2-console/**");
|
||||||
return user.isPresent();
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
// If there's any error validating the API
|
csrfConfig
|
||||||
// key, don't ignore CSRF
|
.csrfTokenRepository(cookieRepo)
|
||||||
return false;
|
.csrfTokenRequestHandler(requestHandler);
|
||||||
}
|
});
|
||||||
})
|
|
||||||
.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.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
http.sessionManagement(
|
http.sessionManagement(
|
||||||
@ -215,6 +235,9 @@ public class SecurityConfiguration {
|
|||||||
|| trimmedUri.startsWith("/images/")
|
|| trimmedUri.startsWith("/images/")
|
||||||
|| trimmedUri.startsWith("/public/")
|
|| trimmedUri.startsWith("/public/")
|
||||||
|| trimmedUri.startsWith("/css/")
|
|| trimmedUri.startsWith("/css/")
|
||||||
|
|| (isH2ConsoleEnabled()
|
||||||
|
&& trimmedUri.startsWith(
|
||||||
|
"/h2-console/"))
|
||||||
|| trimmedUri.startsWith("/fonts/")
|
|| trimmedUri.startsWith("/fonts/")
|
||||||
|| trimmedUri.startsWith("/js/")
|
|| trimmedUri.startsWith("/js/")
|
||||||
|| trimmedUri.startsWith(
|
|| trimmedUri.startsWith(
|
||||||
@ -323,4 +346,8 @@ public class SecurityConfiguration {
|
|||||||
public PersistentTokenRepository persistentTokenRepository() {
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
return new JPATokenRepositoryImpl(persistentLoginRepository);
|
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 java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
@ -14,11 +13,16 @@ import jakarta.transaction.Transactional;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.model.Organization;
|
||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
import stirling.software.proprietary.security.config.PremiumEndpoint;
|
import stirling.software.proprietary.security.config.PremiumEndpoint;
|
||||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||||
import stirling.software.proprietary.security.model.User;
|
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.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;
|
import stirling.software.proprietary.security.service.TeamService;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@ -31,20 +35,33 @@ public class TeamController {
|
|||||||
|
|
||||||
private final TeamRepository teamRepository;
|
private final TeamRepository teamRepository;
|
||||||
private final UserRepository userRepository;
|
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")
|
@PostMapping("/create")
|
||||||
public RedirectView createTeam(@RequestParam("name") String name) {
|
public RedirectView createTeam(
|
||||||
if (teamRepository.existsByNameIgnoreCase(name)) {
|
@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");
|
return new RedirectView("/teams?messageType=teamExists");
|
||||||
}
|
}
|
||||||
Team team = new Team();
|
Team team = new Team();
|
||||||
team.setName(name);
|
team.setName(name);
|
||||||
|
team.setOrganization(organization);
|
||||||
teamRepository.save(team);
|
teamRepository.save(team);
|
||||||
return new RedirectView("/teams?messageType=teamCreated");
|
return new RedirectView("/teams?messageType=teamCreated");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
|
||||||
@PostMapping("/rename")
|
@PostMapping("/rename")
|
||||||
public RedirectView renameTeam(
|
public RedirectView renameTeam(
|
||||||
@RequestParam("teamId") Long teamId, @RequestParam("newName") String newName) {
|
@RequestParam("teamId") Long teamId, @RequestParam("newName") String newName) {
|
||||||
@ -52,10 +69,16 @@ public class TeamController {
|
|||||||
if (existing.isEmpty()) {
|
if (existing.isEmpty()) {
|
||||||
return new RedirectView("/teams?messageType=teamNotFound");
|
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");
|
return new RedirectView("/teams?messageType=teamNameExists");
|
||||||
}
|
}
|
||||||
Team team = existing.get();
|
|
||||||
|
|
||||||
// Prevent renaming the Internal team
|
// Prevent renaming the Internal team
|
||||||
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
@ -67,7 +90,6 @@ public class TeamController {
|
|||||||
return new RedirectView("/teams?messageType=teamRenamed");
|
return new RedirectView("/teams?messageType=teamRenamed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
|
||||||
@PostMapping("/delete")
|
@PostMapping("/delete")
|
||||||
@Transactional
|
@Transactional
|
||||||
public RedirectView deleteTeam(@RequestParam("teamId") Long teamId) {
|
public RedirectView deleteTeam(@RequestParam("teamId") Long teamId) {
|
||||||
@ -78,6 +100,10 @@ public class TeamController {
|
|||||||
|
|
||||||
Team team = teamOpt.get();
|
Team team = teamOpt.get();
|
||||||
|
|
||||||
|
if (!authorizationService.canManageTeam(team)) {
|
||||||
|
return new RedirectView("/teams?messageType=accessDenied");
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent deleting the Internal team
|
// Prevent deleting the Internal team
|
||||||
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
return new RedirectView("/teams?messageType=internalTeamNotAccessible");
|
return new RedirectView("/teams?messageType=internalTeamNotAccessible");
|
||||||
@ -92,7 +118,6 @@ public class TeamController {
|
|||||||
return new RedirectView("/teams?messageType=teamDeleted");
|
return new RedirectView("/teams?messageType=teamDeleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
|
||||||
@PostMapping("/addUser")
|
@PostMapping("/addUser")
|
||||||
@Transactional
|
@Transactional
|
||||||
public RedirectView addUserToTeam(
|
public RedirectView addUserToTeam(
|
||||||
@ -104,6 +129,10 @@ public class TeamController {
|
|||||||
.findById(teamId)
|
.findById(teamId)
|
||||||
.orElseThrow(() -> new RuntimeException("Team not found"));
|
.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
|
// Prevent adding users to the Internal team
|
||||||
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
|
||||||
return new RedirectView("/teams?error=internalTeamNotAccessible");
|
return new RedirectView("/teams?error=internalTeamNotAccessible");
|
||||||
@ -121,6 +150,13 @@ public class TeamController {
|
|||||||
return new RedirectView("/teams/" + teamId + "?error=cannotMoveInternalUsers");
|
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
|
// Assign user to team
|
||||||
user.setTeam(team);
|
user.setTeam(team);
|
||||||
userRepository.save(user);
|
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);
|
long countByTeam(Team team);
|
||||||
|
|
||||||
|
List<User> findByTeam(Team team);
|
||||||
|
|
||||||
List<User> findAllByTeam(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() {
|
public boolean hasPassword() {
|
||||||
return this.password != null && !this.password.isEmpty();
|
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> {
|
public interface TeamRepository extends JpaRepository<Team, Long> {
|
||||||
Optional<Team> findByName(String name);
|
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(
|
@Query(
|
||||||
"SELECT new stirling.software.proprietary.model.dto.TeamWithUserCountDTO(t.id, t.name, COUNT(u)) "
|
"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")
|
+ "FROM Team t LEFT JOIN t.users u GROUP BY t.id, t.name")
|
||||||
List<TeamWithUserCountDTO> findAllTeamsWithUserCount();
|
List<TeamWithUserCountDTO> findAllTeamsWithUserCount();
|
||||||
|
|
||||||
boolean existsByNameIgnoreCase(String name);
|
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 lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.model.Organization;
|
||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||||
|
|
||||||
@ -12,29 +13,46 @@ import stirling.software.proprietary.security.repository.TeamRepository;
|
|||||||
public class TeamService {
|
public class TeamService {
|
||||||
|
|
||||||
private final TeamRepository teamRepository;
|
private final TeamRepository teamRepository;
|
||||||
|
private final OrganizationService organizationService;
|
||||||
|
|
||||||
public static final String DEFAULT_TEAM_NAME = "Default";
|
public static final String DEFAULT_TEAM_NAME = "Default";
|
||||||
public static final String INTERNAL_TEAM_NAME = "Internal";
|
public static final String INTERNAL_TEAM_NAME = "Internal";
|
||||||
|
|
||||||
public Team getOrCreateDefaultTeam() {
|
public Team getOrCreateDefaultTeam() {
|
||||||
|
Organization defaultOrg = organizationService.getOrCreateDefaultOrganization();
|
||||||
return teamRepository
|
return teamRepository
|
||||||
.findByName(DEFAULT_TEAM_NAME)
|
.findByNameAndOrganizationId(DEFAULT_TEAM_NAME, defaultOrg.getId())
|
||||||
.orElseGet(
|
.orElseGet(
|
||||||
() -> {
|
() -> {
|
||||||
Team defaultTeam = new Team();
|
Team defaultTeam = new Team();
|
||||||
defaultTeam.setName(DEFAULT_TEAM_NAME);
|
defaultTeam.setName(DEFAULT_TEAM_NAME);
|
||||||
|
defaultTeam.setOrganization(defaultOrg);
|
||||||
return teamRepository.save(defaultTeam);
|
return teamRepository.save(defaultTeam);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Team getOrCreateInternalTeam() {
|
public Team getOrCreateInternalTeam() {
|
||||||
|
Organization internalOrg = organizationService.getOrCreateInternalOrganization();
|
||||||
return teamRepository
|
return teamRepository
|
||||||
.findByName(INTERNAL_TEAM_NAME)
|
.findByNameAndOrganizationId(INTERNAL_TEAM_NAME, internalOrg.getId())
|
||||||
.orElseGet(
|
.orElseGet(
|
||||||
() -> {
|
() -> {
|
||||||
Team internalTeam = new Team();
|
Team internalTeam = new Team();
|
||||||
internalTeam.setName(INTERNAL_TEAM_NAME);
|
internalTeam.setName(INTERNAL_TEAM_NAME);
|
||||||
|
internalTeam.setOrganization(internalOrg);
|
||||||
return teamRepository.save(internalTeam);
|
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);
|
return userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<User> findByRole(String role) {
|
||||||
|
return userRepository.findByRole(role);
|
||||||
|
}
|
||||||
|
|
||||||
public Authority findRole(User user) {
|
public Authority findRole(User user) {
|
||||||
return authorityRepository.findByUserId(user.getId());
|
return authorityRepository.findByUserId(user.getId());
|
||||||
}
|
}
|
||||||
@ -626,4 +630,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
public void saveAll(List<User> users) {
|
public void saveAll(List<User> users) {
|
||||||
userRepository.saveAll(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.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import stirling.software.proprietary.model.Organization;
|
||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@ -18,15 +21,24 @@ class TeamServiceTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private TeamRepository teamRepository;
|
private TeamRepository teamRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private OrganizationService organizationService;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private TeamService teamService;
|
private TeamService teamService;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getDefaultTeam() {
|
void getDefaultTeam() {
|
||||||
|
var organization = new Organization();
|
||||||
|
organization.setId(1L);
|
||||||
|
organization.setName("Default Organization");
|
||||||
|
|
||||||
var team = new Team();
|
var team = new Team();
|
||||||
team.setName("Marleyans");
|
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));
|
.thenReturn(Optional.of(team));
|
||||||
|
|
||||||
Team result = teamService.getOrCreateDefaultTeam();
|
Team result = teamService.getOrCreateDefaultTeam();
|
||||||
@ -36,12 +48,18 @@ class TeamServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createDefaultTeam_whenRepositoryIsEmpty() {
|
void createDefaultTeam_whenRepositoryIsEmpty() {
|
||||||
|
var organization = new Organization();
|
||||||
|
organization.setId(1L);
|
||||||
|
organization.setName("Default Organization");
|
||||||
|
|
||||||
String teamName = "Default";
|
String teamName = "Default";
|
||||||
var defaultTeam = new Team();
|
var defaultTeam = new Team();
|
||||||
defaultTeam.setId(1L);
|
defaultTeam.setId(1L);
|
||||||
defaultTeam.setName(teamName);
|
defaultTeam.setName(teamName);
|
||||||
|
defaultTeam.setOrganization(organization);
|
||||||
|
|
||||||
when(teamRepository.findByName(teamName))
|
when(organizationService.getOrCreateDefaultOrganization()).thenReturn(organization);
|
||||||
|
when(teamRepository.findByNameAndOrganizationId(teamName, organization.getId()))
|
||||||
.thenReturn(Optional.empty());
|
.thenReturn(Optional.empty());
|
||||||
when(teamRepository.save(any(Team.class))).thenReturn(defaultTeam);
|
when(teamRepository.save(any(Team.class))).thenReturn(defaultTeam);
|
||||||
|
|
||||||
@ -52,10 +70,16 @@ class TeamServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getInternalTeam() {
|
void getInternalTeam() {
|
||||||
|
var organization = new Organization();
|
||||||
|
organization.setId(2L);
|
||||||
|
organization.setName("Internal Organization");
|
||||||
|
|
||||||
var team = new Team();
|
var team = new Team();
|
||||||
team.setName("Eldians");
|
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));
|
.thenReturn(Optional.of(team));
|
||||||
|
|
||||||
Team result = teamService.getOrCreateInternalTeam();
|
Team result = teamService.getOrCreateInternalTeam();
|
||||||
@ -65,16 +89,20 @@ class TeamServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createInternalTeam_whenRepositoryIsEmpty() {
|
void createInternalTeam_whenRepositoryIsEmpty() {
|
||||||
|
var organization = new Organization();
|
||||||
|
organization.setId(2L);
|
||||||
|
organization.setName("Internal Organization");
|
||||||
|
|
||||||
String teamName = "Internal";
|
String teamName = "Internal";
|
||||||
Team internalTeam = new Team();
|
Team internalTeam = new Team();
|
||||||
internalTeam.setId(2L);
|
internalTeam.setId(2L);
|
||||||
internalTeam.setName(teamName);
|
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());
|
.thenReturn(Optional.empty());
|
||||||
when(teamRepository.save(any(Team.class))).thenReturn(internalTeam);
|
when(teamRepository.save(any(Team.class))).thenReturn(internalTeam);
|
||||||
when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME))
|
|
||||||
.thenReturn(Optional.empty());
|
|
||||||
|
|
||||||
Team result = teamService.getOrCreateInternalTeam();
|
Team result = teamService.getOrCreateInternalTeam();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user