Table of Contents
- Introduction to Method Security
- Project Setup and Dependencies
- Basic @PreAuthorize Usage
- Advanced Expression Patterns
- Custom Security Expressions
- Testing Method Security
- Best Practices and Common Patterns
Introduction to Method Security
Method-level security in Spring Boot provides fine-grained access control to individual methods in your application. The @PreAuthorize annotation allows you to define security expressions that are evaluated before method execution, ensuring only authorized users can invoke specific operations.
Key Benefits:
- Fine-grained control: Secure individual methods
- Expression-based: Powerful SpEL expressions for complex rules
- Integration with Spring Security: Seamless authentication and authorization
- Business logic separation: Keep security concerns separate from business logic
Project Setup and Dependencies
1. Maven Dependencies
<!-- pom.xml --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies>
2. Security Configuration
// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin"))
.roles("USER", "ADMIN")
.build();
UserDetails moderator = User.builder()
.username("moderator")
.password(passwordEncoder().encode("moderator"))
.roles("USER", "MODERATOR")
.build();
return new InMemoryUserDetailsManager(user, admin, moderator);
}
}
3. Domain Models
// User.java
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String role;
@Column(name = "created_by")
private String createdBy;
// Constructors, getters, and setters
public User() {}
public User(String username, String password, String email, String role) {
this.username = username;
this.password = password;
this.email = email;
this.role = role;
}
// Getters and setters...
}
// Product.java
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String description;
@Column(nullable = false)
private Double price;
@Column(name = "created_by")
private String createdBy;
@Column(name = "is_premium")
private Boolean isPremium = false;
// Constructors, getters, and setters
public Product() {}
public Product(String name, String description, Double price, String createdBy) {
this.name = name;
this.description = description;
this.price = price;
this.createdBy = createdBy;
}
// Getters and setters...
}
Basic @PreAuthorize Usage
1. Role-Based Security
// UserService.java
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')")
public User updateUser(Long userId, User userDetails) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
user.setEmail(userDetails.getEmail());
return userRepository.save(user);
}
@PreAuthorize("hasAuthority('READ_PRIVILEGE')")
public User getUserById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
}
@PreAuthorize("isAuthenticated()")
public User getCurrentUserProfile() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
}
}
2. Permission-Based Security
// ProductService.java
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@PreAuthorize("hasPermission(#productId, 'Product', 'READ')")
public Product getProductById(Long productId) {
return productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
}
@PreAuthorize("hasPermission(#product, 'WRITE')")
public Product createProduct(Product product) {
// Set the creator
String username = SecurityContextHolder.getContext().getAuthentication().getName();
product.setCreatedBy(username);
return productRepository.save(product);
}
@PreAuthorize("hasPermission(#productId, 'Product', 'DELETE')")
public void deleteProduct(Long productId) {
productRepository.deleteById(productId);
}
@PreAuthorize("hasPermission(#productId, 'Product', 'UPDATE')")
public Product updateProduct(Long productId, Product productDetails) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
product.setName(productDetails.getName());
product.setDescription(productDetails.getDescription());
product.setPrice(productDetails.getPrice());
return productRepository.save(product);
}
}
Advanced Expression Patterns
1. Complex Business Rules
// OrderService.java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final UserRepository userRepository;
public OrderService(OrderRepository orderRepository, UserRepository userRepository) {
this.orderRepository = orderRepository;
this.userRepository = userRepository;
}
// User can only access their own orders unless they're ADMIN
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public List<Order> getUserOrders(Long userId) {
return orderRepository.findByUserId(userId);
}
// Users can only cancel their own pending orders
@PreAuthorize("#order.userId == authentication.principal.id and #order.status == 'PENDING'")
public void cancelOrder(Order order) {
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
// Premium product access control
@PreAuthorize("[email protected](#productId) or hasRole('PREMIUM_USER')")
public Product accessProduct(Long productId) {
return productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found"));
}
// Time-based access control
@PreAuthorize("hasRole('ADMIN') or (T(java.time.LocalTime).now().isAfter(T(java.time.LocalTime).of(9, 0)) and T(java.time.LocalTime).now().isBefore(T(java.time.LocalTime).of(17, 0)))")
public void performBusinessOperation() {
// Business logic during business hours only
}
}
2. Parameter-Based Security
// DocumentService.java
@Service
public class DocumentService {
private final DocumentRepository documentRepository;
public DocumentService(DocumentRepository documentRepository) {
this.documentRepository = documentRepository;
}
// Users can only access documents they own or are shared with them
@PreAuthorize("#document.owner == authentication.name or #document.sharedUsers.contains(authentication.name)")
public Document getDocument(Document document) {
return document;
}
// Complex permission checking with multiple conditions
@PreAuthorize("(hasRole('ADMIN') and #sensitive) or (!#sensitive and isAuthenticated())")
public String accessData(boolean sensitive) {
return sensitive ? "Sensitive Data" : "Public Data";
}
// Method parameter validation with security
@PreAuthorize("#amount <= 1000 or hasRole('APPROVER')")
public void processPayment(Double amount) {
// Process payment logic
}
}
Custom Security Expressions
1. Custom Permission Evaluator
// CustomPermissionEvaluator.java
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
private final UserRepository userRepository;
private final ProductRepository productRepository;
public CustomPermissionEvaluator(UserRepository userRepository, ProductRepository productRepository) {
this.userRepository = userRepository;
this.productRepository = productRepository;
}
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
String username = authentication.getName();
String permissionStr = (String) permission;
// Handle different domain objects
if (targetDomainObject instanceof Product) {
return hasProductPermission((Product) targetDomainObject, username, permissionStr);
} else if (targetDomainObject instanceof User) {
return hasUserPermission((User) targetDomainObject, username, permissionStr);
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}
String username = authentication.getName();
String permissionStr = (String) permission;
switch (targetType) {
case "Product":
Product product = productRepository.findById((Long) targetId)
.orElseThrow(() -> new RuntimeException("Product not found"));
return hasProductPermission(product, username, permissionStr);
case "User":
User user = userRepository.findById((Long) targetId)
.orElseThrow(() -> new RuntimeException("User not found"));
return hasUserPermission(user, username, permissionStr);
default:
return false;
}
}
private boolean hasProductPermission(Product product, String username, String permission) {
switch (permission) {
case "READ":
return !product.getIsPremium() ||
userRepository.findByUsername(username)
.map(u -> "PREMIUM_USER".equals(u.getRole()))
.orElse(false);
case "WRITE":
case "UPDATE":
case "DELETE":
return product.getCreatedBy().equals(username) ||
userRepository.findByUsername(username)
.map(u -> "ADMIN".equals(u.getRole()))
.orElse(false);
default:
return false;
}
}
private boolean hasUserPermission(User user, String username, String permission) {
User currentUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
switch (permission) {
case "READ":
return user.getUsername().equals(username) || "ADMIN".equals(currentUser.getRole());
case "UPDATE":
case "DELETE":
return "ADMIN".equals(currentUser.getRole());
default:
return false;
}
}
}
2. Custom Security Expression Handler
// CustomMethodSecurityExpressionHandler.java
@Component
public class CustomMethodSecurityExpressionHandler extends DefaultMethodSecurityExpressionHandler {
private final CustomSecurityExpression customSecurityExpression;
public CustomMethodSecurityExpressionHandler(CustomSecurityExpression customSecurityExpression) {
this.customSecurityExpression = customSecurityExpression;
}
@Override
protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
Authentication authentication, MethodInvocation invocation) {
CustomMethodSecurityExpressionRoot root = new CustomMethodSecurityExpressionRoot(authentication);
root.setCustomSecurityExpression(customSecurityExpression);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(getTrustResolver());
root.setRoleHierarchy(getRoleHierarchy());
return root;
}
}
// CustomSecurityExpression.java
@Component
public class CustomSecurityExpression {
private final UserRepository userRepository;
public CustomSecurityExpression(UserRepository userRepository) {
this.userRepository = userRepository;
}
public boolean isResourceOwner(Long resourceId, String resourceType) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
switch (resourceType) {
case "PRODUCT":
// Implementation to check if user owns the product
return true; // Simplified for example
case "ORDER":
// Implementation to check if user owns the order
return true; // Simplified for example
default:
return false;
}
}
public boolean isInSameDepartment(String targetUsername) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentUsername = authentication.getName();
User currentUser = userRepository.findByUsername(currentUsername)
.orElseThrow(() -> new RuntimeException("User not found"));
User targetUser = userRepository.findByUsername(targetUsername)
.orElseThrow(() -> new RuntimeException("Target user not found"));
// Assuming users have department field
return currentUser.getDepartment().equals(targetUser.getDepartment());
}
public boolean hasAccessToPremiumFeature() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
return "PREMIUM_USER".equals(user.getRole()) || "ADMIN".equals(user.getRole());
}
}
// CustomMethodSecurityExpressionRoot.java
public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
private CustomSecurityExpression customSecurityExpression;
private Object filterObject;
private Object returnObject;
private Object target;
public CustomMethodSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
public void setCustomSecurityExpression(CustomSecurityExpression customSecurityExpression) {
this.customSecurityExpression = customSecurityExpression;
}
public boolean isResourceOwner(Long resourceId, String resourceType) {
return customSecurityExpression.isResourceOwner(resourceId, resourceType);
}
public boolean isInSameDepartment(String targetUsername) {
return customSecurityExpression.isInSameDepartment(targetUsername);
}
public boolean hasAccessToPremiumFeature() {
return customSecurityExpression.hasAccessToPremiumFeature();
}
@Override
public void setFilterObject(Object filterObject) {
this.filterObject = filterObject;
}
@Override
public Object getFilterObject() {
return filterObject;
}
@Override
public void setReturnObject(Object returnObject) {
this.returnObject = returnObject;
}
@Override
public Object getReturnObject() {
return returnObject;
}
@Override
public Object getThis() {
return target;
}
}
3. Using Custom Expressions in Services
// AdvancedSecurityService.java
@Service
public class AdvancedSecurityService {
@PreAuthorize("@customSecurityExpression.isResourceOwner(#productId, 'PRODUCT')")
public Product updateProductDetails(Long productId, Product productDetails) {
// Update logic
return productRepository.save(productDetails);
}
@PreAuthorize("@customSecurityExpression.isInSameDepartment(#username)")
public User getColleagueInfo(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
}
@PreAuthorize("@customSecurityExpression.hasAccessToPremiumFeature()")
public String accessPremiumContent() {
return "Premium content accessed successfully";
}
// Using custom expression root methods
@PreAuthorize("isResourceOwner(#documentId, 'DOCUMENT')")
public Document shareDocument(Long documentId, String shareWith) {
Document document = documentRepository.findById(documentId)
.orElseThrow(() -> new RuntimeException("Document not found"));
// Sharing logic
return document;
}
}
Testing Method Security
1. Security Test Configuration
// TestSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TestSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
return http.build();
}
}
2. Service Layer Tests
// UserServiceTest.java
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Import(TestSecurityConfig.class)
class UserServiceTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
@WithMockUser(roles = "ADMIN")
void getAllUsers_WithAdminRole_ShouldSucceed() {
// Given
User user = new User("testuser", "password", "[email protected]", "USER");
userRepository.save(user);
// When
List<User> users = userService.getAllUsers();
// Then
assertFalse(users.isEmpty());
}
@Test
@WithMockUser(roles = "USER")
void getAllUsers_WithUserRole_ShouldThrowAccessDenied() {
// When & Then
assertThrows(AccessDeniedException.class, () -> userService.getAllUsers());
}
@Test
@WithMockUser(username = "testuser")
void getCurrentUserProfile_WithAuthenticatedUser_ShouldSucceed() {
// Given
User user = new User("testuser", "password", "[email protected]", "USER");
userRepository.save(user);
// When
User result = userService.getCurrentUserProfile();
// Then
assertEquals("testuser", result.getUsername());
}
@Test
@WithAnonymousUser
void getCurrentUserProfile_WithAnonymousUser_ShouldThrowAccessDenied() {
assertThrows(AccessDeniedException.class, () -> userService.getCurrentUserProfile());
}
}
3. Complex Security Rule Tests
// ProductServiceTest.java
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Import(TestSecurityConfig.class)
class ProductServiceTest {
@Autowired
private ProductService productService;
@Autowired
private ProductRepository productRepository;
@Test
@WithMockUser(username = "owner")
void updateProduct_WhenUserIsOwner_ShouldSucceed() {
// Given
Product product = new Product("Test Product", "Description", 99.99, "owner");
Product savedProduct = productRepository.save(product);
Product updateDetails = new Product("Updated Product", "New Description", 129.99, "owner");
// When
Product result = productService.updateProduct(savedProduct.getId(), updateDetails);
// Then
assertEquals("Updated Product", result.getName());
}
@Test
@WithMockUser(username = "otheruser")
void updateProduct_WhenUserIsNotOwner_ShouldThrowAccessDenied() {
// Given
Product product = new Product("Test Product", "Description", 99.99, "owner");
Product savedProduct = productRepository.save(product);
Product updateDetails = new Product("Updated Product", "New Description", 129.99, "owner");
// When & Then
assertThrows(AccessDeniedException.class,
() -> productService.updateProduct(savedProduct.getId(), updateDetails));
}
@Test
@WithMockUser(roles = "ADMIN")
void deleteProduct_WithAdminRole_ShouldSucceed() {
// Given
Product product = new Product("Test Product", "Description", 99.99, "someuser");
Product savedProduct = productRepository.save(product);
// When & Then
assertDoesNotThrow(() -> productService.deleteProduct(savedProduct.getId()));
}
}
Best Practices and Common Patterns
1. Security Configuration Best Practices
// SecurityBestPractices.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class SecurityBestPractices {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Always use CSRF protection for stateful applications
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/public/**")
)
// Configure CORS properly
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// Session management
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
)
// Exception handling
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.accessDeniedHandler(new HttpStatusEntryPoint(HttpStatus.FORBIDDEN))
)
// Authorization rules
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://trusted-domain.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
2. Service Layer Security Patterns
// SecureBusinessService.java
@Service
@Transactional
public class SecureBusinessService {
// Pattern 1: Role-based access with business logic
@PreAuthorize("hasRole('APPROVER')")
@PostAuthorize("returnObject.status == 'APPROVED' or returnObject.amount < 1000")
public Transaction approveTransaction(Transaction transaction) {
if (transaction.getAmount() > 5000) {
transaction.setStatus(TransactionStatus.REQUIRES_MANAGER_APPROVAL);
} else {
transaction.setStatus(TransactionStatus.APPROVED);
}
return transactionRepository.save(transaction);
}
// Pattern 2: Ownership-based access
@PreAuthorize("#report.owner == authentication.name or hasRole('MANAGER')")
public Report updateReport(Report report) {
return reportRepository.save(report);
}
// Pattern 3: Time-based restrictions
@PreAuthorize("hasRole('ADMIN') or (T(java.time.DayOfWeek).from(T(java.time.LocalDate).now()).getValue() < 6)")
public void performWeekdayOperation() {
// Operation that can only be performed on weekdays by non-admin users
}
// Pattern 4: Complex business rules with custom expressions
@PreAuthorize("@securityUtils.canAccessProject(#projectId, authentication.name)")
public Project getProject(Long projectId) {
return projectRepository.findById(projectId)
.orElseThrow(() -> new RuntimeException("Project not found"));
}
// Pattern 5: Hierarchical permission checking
@PreAuthorize("hasPermission(#departmentId, 'Department', 'MANAGE')")
public Department manageDepartment(Long departmentId, Department departmentDetails) {
Department department = departmentRepository.findById(departmentId)
.orElseThrow(() -> new RuntimeException("Department not found"));
department.setName(departmentDetails.getName());
return departmentRepository.save(department);
}
}
3. Common Security Utility Class
// SecurityUtils.java
@Component
public class SecurityUtils {
private final UserRepository userRepository;
public SecurityUtils(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getCurrentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new AccessDeniedException("User not authenticated");
}
return authentication.getName();
}
public User getCurrentUser() {
String username = getCurrentUsername();
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
}
public boolean hasRole(String role) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.getAuthorities().stream()
.anyMatch(authority -> authority.getAuthority().equals("ROLE_" + role));
}
public boolean canAccessProject(Long projectId, String username) {
// Complex business logic for project access
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
// Implementation depends on your business rules
return true; // Simplified for example
}
}
4. Error Handling and Logging
// SecurityAspect.java
@Aspect
@Component
public class SecurityAspect {
private static final Logger logger = LoggerFactory.getLogger(SecurityAspect.class);
@AfterThrowing(pointcut = "@annotation(preAuthorize)", throwing = "ex")
public void logSecurityException(PreAuthorize preAuthorize, AccessDeniedException ex) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String method = ((MethodSignature) JointPoint.current().getSignature()).getMethod().getName();
logger.warn("Security violation - User: {}, Method: {}, Expression: {}",
username, method, preAuthorize.value());
// You can also send alerts or metrics here
}
}
Conclusion
Method-level security with @PreAuthorize provides a powerful and flexible way to secure your Spring Boot application. By leveraging SpEL expressions, custom permission evaluators, and integration with Spring Security, you can implement complex security rules that are closely aligned with your business requirements.
Key Takeaways:
- Start Simple: Begin with role-based security and gradually add complexity
- Use Custom Expressions: Create reusable security expressions for complex business rules
- Test Thoroughly: Always test security rules with different user roles and scenarios
- Keep Security Separate: Maintain separation between security concerns and business logic
- Monitor and Log: Implement proper logging and monitoring for security events
Remember that security is an ongoing process. Regularly review and update your security rules as your application evolves and new requirements emerge.