diff --git a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/configuration/WebSocketSecurityConfig.java b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/configuration/WebSocketSecurityConfig.java new file mode 100644 index 000000000..d701de243 --- /dev/null +++ b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/configuration/WebSocketSecurityConfig.java @@ -0,0 +1,88 @@ +package com.iqser.red.service.persistence.management.v1.processor.configuration; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; +import org.springframework.messaging.simp.SimpMessageType; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; +import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import com.knecon.fforesight.keycloakcommons.security.TokenUtils; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Configuration +public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { + + @Value("${cors.enabled:false}") + private boolean corsEnabled; + + + @Override + protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { + + messages.simpTypeMatchers(SimpMessageType.HEARTBEAT, SimpMessageType.UNSUBSCRIBE, SimpMessageType.DISCONNECT) + .permitAll() + .simpTypeMatchers(SimpMessageType.CONNECT) + .anonymous() // this is intended, see WebSocketConfiguration.configureClientInboundChannel + .simpTypeMatchers(SimpMessageType.SUBSCRIBE) + .access("@tenantWebSocketSecurityMatcher.checkCanSubscribeTo(authentication,message)") + .anyMessage() + .denyAll(); + } + + + @Override + protected boolean sameOriginDisabled() { + + return corsEnabled; + } + + + @Bean + public TenantWebSocketSecurityMatcher tenantWebSocketSecurityMatcher() { + + return new TenantWebSocketSecurityMatcher(); + } + + + public class TenantWebSocketSecurityMatcher { + + public boolean checkCanSubscribeTo(JwtAuthenticationToken authentication, Message message) { + + var targetedTenant = extractTenantId(message); + var currentTenant = getCurrentTenant(authentication); + return targetedTenant.isPresent() && currentTenant.isPresent() && currentTenant.get().equals(targetedTenant.get()); + } + + + private Optional getCurrentTenant(JwtAuthenticationToken authentication) { + + if (authentication != null && authentication.getToken() != null && authentication.getToken().getTokenValue() != null) { + return Optional.of(TokenUtils.toTenant(authentication.getToken().getTokenValue())); + } else { + return Optional.empty(); + } + } + + } + + private Optional extractTenantId(Message message) { + + StompHeaderAccessor sha = StompHeaderAccessor.wrap(message); + String topic = sha.getDestination(); + if (topic == null) { + return Optional.empty(); + } + + String tenant = topic.split("/")[2]; + return Optional.of(tenant); + } + +}