From 9735f1f39892e13b69e16bca3b4f3c170d7f21de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Murat=20=C3=96zkorkmaz?= Date: Thu, 28 Aug 2025 12:57:36 +0200 Subject: [PATCH] Initial commit with basic CRUD functionality: * GET all properties * GET one property by id * CREATE one property * DELETE one property by id --- bruno/propify/Authenticate/Auth.bru | 48 +++++++++++++++++++ bruno/propify/Authenticate/folder.bru | 8 ++++ bruno/propify/Properties/Create new.bru | 8 +++- bruno/propify/Properties/Delete one by ID.bru | 8 +++- bruno/propify/Properties/Get all.bru | 8 +++- bruno/propify/Properties/Get one by ID.bru | 8 +++- bruno/propify/environments/local.bru | 9 +++- pom.xml | 9 ++++ .../controller/PropertyController.java | 11 +++++ .../propify_api/security/CorsConfig.java | 28 +++++++++++ .../propify_api/security/CorsProperties.java | 29 +++++++++++ .../security/KeycloakRoleConverter.java | 33 +++++++++++++ .../propify_api/security/SecurityConfig.java | 35 ++++++++++---- src/main/resources/application.yml | 17 +++++++ 14 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 bruno/propify/Authenticate/Auth.bru create mode 100644 bruno/propify/Authenticate/folder.bru create mode 100644 src/main/java/de/iwomm/propify_api/security/CorsConfig.java create mode 100644 src/main/java/de/iwomm/propify_api/security/CorsProperties.java create mode 100644 src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java diff --git a/bruno/propify/Authenticate/Auth.bru b/bruno/propify/Authenticate/Auth.bru new file mode 100644 index 0000000..2b1b156 --- /dev/null +++ b/bruno/propify/Authenticate/Auth.bru @@ -0,0 +1,48 @@ +meta { + name: Auth + type: http + seq: 2 +} + +post { + url: {{KEYCLOAK_BASE_URL}}/realms/propify/protocol/openid-connect/token + body: formUrlEncoded + auth: inherit +} + +headers { + Content-Type: application/x-www-form-urlencoded +} + +body:form-urlencoded { + grant_type: password + client_id: {{KEYCLOAK_CLIENT_ID}} + username: {{ADMIN_USERNAME}} + password: {{ADMIN_PASSWORD}} +} + +script:post-response { + // Parse die JSON-Antwort + const jsonResponse = res.body; + + + if (jsonResponse.access_token) { + // Schreibe den access_token in eine Umgebungsvariable + // oder in eine collection-Variable + + // Option 1: In eine Umgebungsvariable schreiben + // (z.B. für eine bestimmte Umgebung wie "Development") + bru.setEnvVar("BEARER_TOKEN", jsonResponse.access_token); + + // Option 2: In eine Collection-Variable schreiben + // (Diese Variable ist global für alle Anfragen in deiner Collection) + // bru.setVar("bearerToken", "Bearer " + jsonResponse.access_token); + } else { + // optional: Error Handling, falls der Token nicht in der Antwort ist + console.log("Error: access_token not found in the response."); + } +} + +settings { + encodeUrl: false +} diff --git a/bruno/propify/Authenticate/folder.bru b/bruno/propify/Authenticate/folder.bru new file mode 100644 index 0000000..196f590 --- /dev/null +++ b/bruno/propify/Authenticate/folder.bru @@ -0,0 +1,8 @@ +meta { + name: Authenticate + seq: 2 +} + +auth { + mode: inherit +} diff --git a/bruno/propify/Properties/Create new.bru b/bruno/propify/Properties/Create new.bru index d2ad449..a79b3d5 100644 --- a/bruno/propify/Properties/Create new.bru +++ b/bruno/propify/Properties/Create new.bru @@ -5,9 +5,13 @@ meta { } post { - url: {{BASE_URL}}/api/{{API_VERSION}}/properties + url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties body: json - auth: inherit + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} } body:json { diff --git a/bruno/propify/Properties/Delete one by ID.bru b/bruno/propify/Properties/Delete one by ID.bru index d71ff16..c5057b9 100644 --- a/bruno/propify/Properties/Delete one by ID.bru +++ b/bruno/propify/Properties/Delete one by ID.bru @@ -5,9 +5,13 @@ meta { } delete { - url: {{BASE_URL}}/api/{{API_VERSION}}/properties/e64d5e53-cd45-45fb-9237-46078077bf22 + url: API_BASE_URL}}/api/{{API_VERSION}}/properties/e64d5e53-cd45-45fb-9237-46078077bf22 body: json - auth: inherit + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} } body:json { diff --git a/bruno/propify/Properties/Get all.bru b/bruno/propify/Properties/Get all.bru index 919b56a..9ae00d5 100644 --- a/bruno/propify/Properties/Get all.bru +++ b/bruno/propify/Properties/Get all.bru @@ -5,9 +5,13 @@ meta { } get { - url: {{BASE_URL}}/api/{{API_VERSION}}/properties + url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties body: none - auth: inherit + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} } settings { diff --git a/bruno/propify/Properties/Get one by ID.bru b/bruno/propify/Properties/Get one by ID.bru index 018a2a9..8631471 100644 --- a/bruno/propify/Properties/Get one by ID.bru +++ b/bruno/propify/Properties/Get one by ID.bru @@ -5,9 +5,13 @@ meta { } get { - url: {{BASE_URL}}/api/{{API_VERSION}}/properties/b6ab79d8-f7eb-4fb7-acde-e3310cb0166c + url: {{API_BASE_URL}}/api/{{API_VERSION}}/properties/5dba067e-d7fd-4d79-a08a-ec379834938a body: none - auth: inherit + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} } settings { diff --git a/bruno/propify/environments/local.bru b/bruno/propify/environments/local.bru index 1d1d7b3..de3d934 100644 --- a/bruno/propify/environments/local.bru +++ b/bruno/propify/environments/local.bru @@ -1,4 +1,11 @@ vars { - BASE_URL: http://localhost:8080 + API_BASE_URL: http://localhost:8080 API_VERSION: v1 + BEARER_TOKEN: - + KEYCLOAK_BASE_URL: http://localhost:8280 + DEV_USERNAME: dev@example.com + DEV_PASSWORD: dev + ADMIN_USERNAME: admin@example.com + ADMIN_PASSWORD: admin + KEYCLOAK_CLIENT_ID: propify-app } diff --git a/pom.xml b/pom.xml index 2e2fc54..d3cb5cf 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,15 @@ 2.6.0 + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-security + + diff --git a/src/main/java/de/iwomm/propify_api/controller/PropertyController.java b/src/main/java/de/iwomm/propify_api/controller/PropertyController.java index 01b5489..809bdfa 100644 --- a/src/main/java/de/iwomm/propify_api/controller/PropertyController.java +++ b/src/main/java/de/iwomm/propify_api/controller/PropertyController.java @@ -3,6 +3,7 @@ package de.iwomm.propify_api.controller; import de.iwomm.propify_api.entity.Property; import de.iwomm.propify_api.service.PropertyService; import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -18,24 +19,34 @@ public class PropertyController { this.propertyService = propertyService; } + @GetMapping("/info") + @PreAuthorize("isAuthenticated()") + public String infoEndpoint() { + return "Hello, you are authenticated!"; + } + @GetMapping + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')") public List getAllProperties() { return propertyService.findAll(); } @GetMapping("/{id}") + @PreAuthorize("hasAnyRole('ADMIN', 'USER')") public Property getPropertyById(@PathVariable UUID id) { return propertyService.findById(id).orElse(null); } @PostMapping @ResponseStatus(HttpStatus.CREATED) + @PreAuthorize("hasRole('ADMIN')") public Property createProperty(@RequestBody Property property) { return propertyService.save(property); } @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) + @PreAuthorize("hasRole('ADMIN')") public void deleteProperty(@PathVariable UUID id) { Optional property = propertyService.findById(id); if (property.isEmpty()) { diff --git a/src/main/java/de/iwomm/propify_api/security/CorsConfig.java b/src/main/java/de/iwomm/propify_api/security/CorsConfig.java new file mode 100644 index 0000000..6705614 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/security/CorsConfig.java @@ -0,0 +1,28 @@ +package de.iwomm.propify_api.security; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + private final CorsProperties corsProperties; + + public CorsConfig(CorsProperties corsProperties) { + this.corsProperties = corsProperties; + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") // Apply rules to all endpoints + .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0])) // This targets the frontend app's URLs (you can allow multiple URLs, e.g. "http://localhost:4200,http://example.com" + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders(corsProperties.getAllowedHeaders()) + .allowCredentials(true); // Allow cookies and authentication headers + } +} \ No newline at end of file diff --git a/src/main/java/de/iwomm/propify_api/security/CorsProperties.java b/src/main/java/de/iwomm/propify_api/security/CorsProperties.java new file mode 100644 index 0000000..fa18893 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/security/CorsProperties.java @@ -0,0 +1,29 @@ +package de.iwomm.propify_api.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@ConfigurationProperties(prefix = "cors") +public class CorsProperties { + private List allowedOrigins; + private String allowedHeaders; + + public List getAllowedOrigins() { + return allowedOrigins; + } + + public void setAllowedOrigins(List allowedOrigins) { + this.allowedOrigins = allowedOrigins; + } + + public String getAllowedHeaders() { + return allowedHeaders; + } + + public void setAllowedHeaders(String allowedHeaders) { + this.allowedHeaders = allowedHeaders; + } +} diff --git a/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java b/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java new file mode 100644 index 0000000..d09984e --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/security/KeycloakRoleConverter.java @@ -0,0 +1,33 @@ +package de.iwomm.propify_api.security; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class KeycloakRoleConverter implements Converter> { + + @Override + public Collection convert(Jwt jwt) { + // Holen Sie sich das "realm_access" Feld aus dem Token + Map realmAccess = (Map) jwt.getClaims().get("realm_access"); + + if (realmAccess == null || realmAccess.isEmpty()) { + return List.of(); + } + + // Holen Sie sich die Liste der Rollen + List roles = (List) realmAccess.get("roles"); + + // Konvertieren Sie die Rollen in Spring Security GrantedAuthority-Objekte + return roles.stream() + .map(roleName -> "ROLE_" + roleName.toUpperCase()) // Empfohlene Namenskonvention + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java b/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java index 10f2339..361dde4 100644 --- a/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java +++ b/src/main/java/de/iwomm/propify_api/security/SecurityConfig.java @@ -2,27 +2,44 @@ package de.iwomm.propify_api.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity +@EnableMethodSecurity // Wichtig für die Verwendung von @PreAuthorize public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf.disable()) // CSRF deaktivieren für APIs - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/**").permitAll() // API ist offen - .anyRequest().permitAll() // Alles andere auch offen + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> {}) + + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/api/**").authenticated() + .anyRequest().permitAll() ) - .httpBasic(httpBasic -> httpBasic.disable()) // Kein Basic Auth - .formLogin(form -> form.disable()); // Kein Login-Formular + + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) + ) + + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); return http.build(); } - -} + // Konvertiert die Keycloak-Rollen (im JWT) in Spring Security Authorities + private JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter()); + return jwtAuthenticationConverter; + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ec39737..872f6f7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,3 +13,20 @@ spring: h2: console: enabled: true + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://localhost:8280/realms/propify + jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs +logging: + level: + org: + springframework: + security: DEBUG + +cors: + allowed-origins: + - http://localhost:4200 + - http://localhost:8080 + allowed-headers: "*" \ No newline at end of file