diff --git a/messages-service/.gitignore b/messages-service/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/messages-service/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/messages-service/pom.xml b/messages-service/pom.xml new file mode 100644 index 0000000..5ff89ee --- /dev/null +++ b/messages-service/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.4 + + + com.sivalabs + messages-service + 0.0.1-SNAPSHOT + messages-service + messages-service + + 17 + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/messages-service/src/main/java/com/sivalabs/messages/MessagesServiceApplication.java b/messages-service/src/main/java/com/sivalabs/messages/MessagesServiceApplication.java new file mode 100644 index 0000000..d44a3c3 --- /dev/null +++ b/messages-service/src/main/java/com/sivalabs/messages/MessagesServiceApplication.java @@ -0,0 +1,13 @@ +package com.sivalabs.messages; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MessagesServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(MessagesServiceApplication.class, args); + } + +} diff --git a/messages-service/src/main/java/com/sivalabs/messages/api/MessageController.java b/messages-service/src/main/java/com/sivalabs/messages/api/MessageController.java new file mode 100644 index 0000000..555fee7 --- /dev/null +++ b/messages-service/src/main/java/com/sivalabs/messages/api/MessageController.java @@ -0,0 +1,43 @@ +package com.sivalabs.messages.api; + +import com.sivalabs.messages.domain.Message; +import com.sivalabs.messages.domain.MessageRepository; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/messages") +class MessageController { + private static final Logger log = LoggerFactory.getLogger(MessageController.class); + + private final MessageRepository messageRepository; + + MessageController(MessageRepository messageRepository) { + this.messageRepository = messageRepository; + } + + @GetMapping + List getMessages() { + return messageRepository.getMessages(); + } + + @PostMapping + Message createMessage(@RequestBody @Valid Message message) { + return messageRepository.createMessage(message); + } + + @PostMapping("/archive") + Map archiveMessages() { + log.info("Archiving all messages"); + return Map.of("status", "success"); + } +} diff --git a/messages-service/src/main/java/com/sivalabs/messages/api/UserInfoController.java b/messages-service/src/main/java/com/sivalabs/messages/api/UserInfoController.java new file mode 100644 index 0000000..45c697b --- /dev/null +++ b/messages-service/src/main/java/com/sivalabs/messages/api/UserInfoController.java @@ -0,0 +1,44 @@ +package com.sivalabs.messages.api; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +class UserInfoController { + + @GetMapping("/api/me") + Map currentUserDetails() { + return getLoginUserDetails(); + } + + Map getLoginUserDetails() { + Map map = new HashMap<>(); + JwtAuthenticationToken authentication = + (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + Jwt jwt = (Jwt) authentication.getPrincipal(); + + map.put("username", jwt.getClaimAsString("preferred_username")); + map.put("email", jwt.getClaimAsString("email")); + map.put("name", jwt.getClaimAsString("name")); + map.put("token", jwt.getTokenValue()); + map.put("authorities", authentication.getAuthorities()); + map.put("roles", getRoles(jwt)); + + return map; + } + + List getRoles(Jwt jwt) { + Map realm_access = (Map) jwt.getClaims().get("realm_access"); + if(realm_access != null && !realm_access.isEmpty()) { + return (List) realm_access.get("roles"); + } + return List.of(); + } +} diff --git a/messages-service/src/main/java/com/sivalabs/messages/config/KeycloakJwtAuthenticationConverter.java b/messages-service/src/main/java/com/sivalabs/messages/config/KeycloakJwtAuthenticationConverter.java new file mode 100644 index 0000000..d5b5bed --- /dev/null +++ b/messages-service/src/main/java/com/sivalabs/messages/config/KeycloakJwtAuthenticationConverter.java @@ -0,0 +1,42 @@ +package com.sivalabs.messages.config; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +class KeycloakJwtAuthenticationConverter implements Converter { + private final Converter> delegate = new JwtGrantedAuthoritiesConverter(); + + @Override + public AbstractAuthenticationToken convert(Jwt jwt) { + List authorityList = extractRoles(jwt); + Collection authorities = delegate.convert(jwt); + if (authorities != null) { + authorityList.addAll(authorities); + } + return new JwtAuthenticationToken(jwt, authorityList); + } + + private List extractRoles(Jwt jwt) { + Map realm_access = (Map) jwt.getClaims().get("realm_access"); + if(realm_access == null || realm_access.isEmpty()) { + return List.of(); + } + List roles = (List) realm_access.get("roles"); + if (roles == null || roles.isEmpty()) { + roles = List.of("ROLE_USER"); + } + return roles.stream() + .filter(role -> role.startsWith("ROLE_")) + .map(SimpleGrantedAuthority::new).collect(Collectors.toList()); + } +} diff --git a/messages-service/src/main/java/com/sivalabs/messages/config/SecurityConfig.java b/messages-service/src/main/java/com/sivalabs/messages/config/SecurityConfig.java new file mode 100644 index 0000000..a3d4143 --- /dev/null +++ b/messages-service/src/main/java/com/sivalabs/messages/config/SecurityConfig.java @@ -0,0 +1,35 @@ +package com.sivalabs.messages.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.CorsConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(c -> + c + .requestMatchers(HttpMethod.GET, "/api/messages").permitAll() + .requestMatchers(HttpMethod.POST, "/api/messages/archive").hasAnyRole("ADMIN") + .anyRequest().authenticated() + ) + .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .cors(CorsConfigurer::disable) + .csrf(CsrfConfigurer::disable) + .oauth2ResourceServer(oauth2 -> + //oauth2.jwt(Customizer.withDefaults()) + oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(new KeycloakJwtAuthenticationConverter())) + ); + return http.build(); + } +} diff --git a/messages-service/src/main/java/com/sivalabs/messages/domain/Message.java b/messages-service/src/main/java/com/sivalabs/messages/domain/Message.java new file mode 100644 index 0000000..7346d36 --- /dev/null +++ b/messages-service/src/main/java/com/sivalabs/messages/domain/Message.java @@ -0,0 +1,56 @@ +package com.sivalabs.messages.domain; + +import jakarta.validation.constraints.NotEmpty; + +import java.time.Instant; + +public class Message { + private Long id; + @NotEmpty + private String content; + @NotEmpty + private String createdBy; + private Instant createdAt; + + public Message() { + } + + public Message(Long id, String content, String createdBy, Instant createdAt) { + this.id = id; + this.content = content; + this.createdBy = createdBy; + this.createdAt = createdAt; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } +} diff --git a/messages-service/src/main/java/com/sivalabs/messages/domain/MessageRepository.java b/messages-service/src/main/java/com/sivalabs/messages/domain/MessageRepository.java new file mode 100644 index 0000000..5f00426 --- /dev/null +++ b/messages-service/src/main/java/com/sivalabs/messages/domain/MessageRepository.java @@ -0,0 +1,41 @@ +package com.sivalabs.messages.domain; + +import jakarta.annotation.PostConstruct; +import org.springframework.stereotype.Repository; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +@Repository +public class MessageRepository { + private static final AtomicLong ID = new AtomicLong(0L); + private static final List MESSAGES = new ArrayList<>(); + + @PostConstruct + void init() { + getDefaultMessages().forEach( p -> { + p.setId(ID.incrementAndGet()); + MESSAGES.add(p); + }); + } + + public List getMessages() { + return MESSAGES; + } + + public Message createMessage(Message message) { + message.setId(ID.incrementAndGet()); + message.setCreatedAt(Instant.now()); + MESSAGES.add(message); + return message; + } + + private List getDefaultMessages() { + List messages = new ArrayList<>(); + messages.add(new Message(null, "Test Message 1", "admin", Instant.now())); + messages.add(new Message(null, "Test Message 2", "admin", Instant.now())); + return messages; + } +} diff --git a/messages-service/src/main/resources/application.properties b/messages-service/src/main/resources/application.properties new file mode 100644 index 0000000..917edb7 --- /dev/null +++ b/messages-service/src/main/resources/application.properties @@ -0,0 +1,4 @@ +spring.application.name=messages-service +server.port=8181 +OAUTH_SERVER=http://localhost:9191/realms/sivalabs +spring.security.oauth2.resourceserver.jwt.issuer-uri=${OAUTH_SERVER} diff --git a/messages-service/src/test/java/com/sivalabs/messages/MessagesServiceApplicationTests.java b/messages-service/src/test/java/com/sivalabs/messages/MessagesServiceApplicationTests.java new file mode 100644 index 0000000..b96230c --- /dev/null +++ b/messages-service/src/test/java/com/sivalabs/messages/MessagesServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.sivalabs.messages; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class MessagesServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/pom.xml b/pom.xml index 904f8a1..e4551e2 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ messages-webapp + messages-service