Skip to content
Snippets Groups Projects
Commit f5091c64 authored by OZGCloud's avatar OZGCloud
Browse files

Merge remote-tracking branch 'origin/master' into RefactorExceptionControllerTests

parents 2993f301 ba669ab6
Branches
Tags
No related merge requests found
Showing
with 407 additions and 29 deletions
......@@ -11,7 +11,7 @@
</parent>
<groupId>de.ozgcloud</groupId>
<artifactId>administration</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.4.0-SNAPSHOT</version>
<name>Administration</name>
<description>Administration Backend Project</description>
......@@ -24,6 +24,9 @@
<testcontainers-keycloak.version>3.2.0</testcontainers-keycloak.version>
<keycloak-admin-client.version>23.0.6</keycloak-admin-client.version>
<mongock.version>5.4.0</mongock.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
<mapstruct-processor.version>1.5.5.Final</mapstruct-processor.version>
</properties>
<dependencies>
......@@ -70,6 +73,17 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- tools -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct-processor.version}</version>
</dependency>
<!-- mongock -->
<dependency>
<groupId>io.mongock</groupId>
......@@ -222,6 +236,29 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct-processor.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
......
......@@ -43,3 +43,30 @@ app.kubernetes.io/namespace: {{ include "app.namespace" . }}
{{- define "app.ozgcloudEnvironment" -}}
{{- required "Environment muss angegeben sein" (.Values.ozgcloud).environment -}}
{{- end -}}
{{- define "app.ssoRealm" -}}
{{ printf "%s-%s-%s" (include "app.ozgcloudBundesland" .) ( include "app.ozgcloudBezeichner" . ) ( include "app.ozgcloudEnvironment" . ) | trunc 63 | trimSuffix "-" }}
{{- end -}}
{{- define "app.ozgcloudBundesland" -}}
{{- required "Bundesland muss angegeben sein" (.Values.ozgcloud).bundesland }}
{{- end -}}
{{- define "app.ozgcloudBezeichner" -}}
{{- required "ozgcloud.bezeichner muss angegeben sein" (.Values.ozgcloud).bezeichner -}}
{{- if lt 27 (len (.Values.ozgcloud).bezeichner) -}}
{{ fail (printf "ozgcloud.bezeichner %s ist zu lang (max. 27 Zeichen)" (.Values.ozgcloud).bezeichner) }}
{{- end -}}
{{- end -}}
{{- define "app.ssoClientName" -}}
{{- if (.Values.sso.keycloak_client).client_name -}}
{{ printf "%s" (.Values.sso.keycloak_client).client_name }}
{{- else -}}
{{ printf "admin" }}
{{- end -}}
{{- end -}}
{{- define "app.ssoServerUrl" -}}
{{- required "sso.serverUrl muss angegeben sein" (.Values.sso).serverUrl -}}
{{- end -}}
\ No newline at end of file
......@@ -61,6 +61,12 @@ spec:
value: "/bindings"
- name: spring_profiles_active
value: {{ include "app.envSpringProfiles" . }}
- name: ozgcloud_oauth2_realm
value: {{ include "app.ssoRealm" . }}
- name: ozgcloud_oauth2_resource
value: {{ include "app.ssoClientName" . }}
- name: ozgcloud_oauth2_auth-server-url
value: {{ include "app.ssoServerUrl" . }}
{{- if not (.Values.database).useExternal }}
- name: spring_data_mongodb_uri
valueFrom:
......
{{- if not (.Values.sso).disableOzgOperator -}}
{{ range $client := (.Values.sso).keycloak_clients }}
{{ $client := (.Values.sso).keycloak_client }}
---
apiVersion: operator.ozgcloud.de/v1
kind: OzgCloudKeycloakClient
......@@ -32,5 +32,4 @@ spec:
{{ toYaml . | indent 4 }}
{{- end }}
{{- end }}
{{ end }}
{{- end -}}
\ No newline at end of file
......@@ -21,18 +21,18 @@
*/
package de.ozgcloud.admin;
import io.mongock.runner.springboot.EnableMongock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import io.mongock.runner.springboot.EnableMongock;
@SpringBootApplication
@EnableConfigServer
@EnableMongock
@EnableMongoRepositories
public class AdministrationApplication {
public static void main(String[] args) {
SpringApplication.run(AdministrationApplication.class, args);
}
......
......@@ -30,7 +30,7 @@ import lombok.Getter;
@Getter
public class Root {
private String javaVersion;
private String buildVersion;
private String version;
private Instant buildTime;
private String buildNumber;
}
......@@ -48,7 +48,7 @@ public class RootController {
return Root.builder()
.javaVersion(System.getProperty("java.version"))
.buildTime(buildProperties.getTime())
.buildVersion(buildProperties.getVersion())
.version(buildProperties.getVersion())
.buildNumber(buildProperties.get("number"))
.build();
}
......
......@@ -21,6 +21,8 @@
*/
package de.ozgcloud.admin;
import de.ozgcloud.admin.common.user.CurrentUserService;
import de.ozgcloud.admin.common.user.UserRole;
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestProperties;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
......@@ -30,6 +32,9 @@ import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Component
@RequiredArgsConstructor
public class RootModelAssembler implements RepresentationModelAssembler<Root, EntityModel<Root>> {
......@@ -37,13 +42,31 @@ public class RootModelAssembler implements RepresentationModelAssembler<Root, En
private final RepositoryRestProperties restProperties;
private final CurrentUserService currentUserService;
@Override
public EntityModel<Root> toModel(Root root) {
var rootLink = WebMvcLinkBuilder.linkTo(RootController.class);
var configLink = rootLink.toUriComponentsBuilder().replacePath(restProperties.getBasePath());
List<Link> links = buildRootModelLinks();
return EntityModel.of(
root,
Link.of(configLink.toUriString(), REL_CONFIGURATION),
rootLink.withSelfRel());
links);
}
List<Link> buildRootModelLinks() {
List<Link> links = new ArrayList<>();
var rootLinkBuilder = WebMvcLinkBuilder.linkTo(RootController.class);
links.add(rootLinkBuilder.withSelfRel());
if (currentUserService.hasRole(UserRole.ADMIN_ADMIN)) {
links.add(buildConfigLink());
}
return links;
}
private Link buildConfigLink() {
var rootLinkBuilder = WebMvcLinkBuilder.linkTo(RootController.class);
return Link.of(
rootLinkBuilder.toUriComponentsBuilder().replacePath(restProperties.getBasePath()).toUriString(),
REL_CONFIGURATION
);
}
}
/*
* Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das
* Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.admin.common.user;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
class CurrentUserHelper {
static final String ROLE_PREFIX = "ROLE_";
private static final Predicate<String> IS_ROLE_PREFIX_MISSING = role -> !role.startsWith(ROLE_PREFIX);
private static final AuthenticationTrustResolver TRUST_RESOLVER = new AuthenticationTrustResolverImpl();
private static final Predicate<Authentication> IS_TRUSTED = auth -> !TRUST_RESOLVER.isAnonymous(auth);
public static boolean hasRole(String role) {
var auth = getAuthentication();
if ((Objects.isNull(auth)) || (Objects.isNull(auth.getPrincipal()))) {
return false;
}
return containsRole(auth.getAuthorities(), role);
}
static boolean containsRole(Collection<? extends GrantedAuthority> authorities, String role) {
if (Objects.isNull(authorities)) {
return false;
}
return authorities.stream().anyMatch(a -> StringUtils.equalsIgnoreCase(addRolePrefixIfMissing(role), a.getAuthority()));
}
static String addRolePrefixIfMissing(String roleToCheck) {
return Optional.ofNullable(roleToCheck)
.filter(IS_ROLE_PREFIX_MISSING)
.map(role -> ROLE_PREFIX + role)
.orElse(roleToCheck);
}
static Authentication getAuthentication() {
return findAuthentication().orElseThrow(() -> new IllegalStateException("No authenticated User found"));
}
private static Optional<Authentication> findAuthentication() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()).filter(IS_TRUSTED);
}
}
/*
* Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das
* Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.admin.common.user;
import org.springframework.stereotype.Service;
@Service
public class CurrentUserService {
public boolean hasRole(String role) {
return CurrentUserHelper.hasRole(role);
}
}
/*
* Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das
* Ministerium für Energiewende, Klimaschutz, Umwelt und Natur
* Zentrales IT-Management
*
* Lizenziert unter der EUPL, Version 1.2 oder - sobald
* diese von der Europäischen Kommission genehmigt wurden -
* Folgeversionen der EUPL ("Lizenz");
* Sie dürfen dieses Werk ausschließlich gemäß
* dieser Lizenz nutzen.
* Eine Kopie der Lizenz finden Sie hier:
*
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* Sofern nicht durch anwendbare Rechtsvorschriften
* gefordert oder in schriftlicher Form vereinbart, wird
* die unter der Lizenz verbreitete Software "so wie sie
* ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
* ausdrücklich oder stillschweigend - verbreitet.
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.admin.common.user;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UserRole {
public static final String ADMIN_ADMIN = "ADMIN_ADMIN";
}
......@@ -19,7 +19,13 @@
*/
package de.ozgcloud.admin.security;
import static java.util.stream.Collectors.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -29,10 +35,15 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import de.ozgcloud.admin.common.user.UserRole;
import de.ozgcloud.admin.environment.OAuth2Properties;
import lombok.RequiredArgsConstructor;
@Configuration
......@@ -43,6 +54,14 @@ public class SecurityConfiguration {
private final AdminAuthenticationEntryPoint authenticationEntryPoint;
private final OAuth2Properties oAuth2Properties;
static final String RESOURCE_ACCESS_KEY = "resource_access";
static final String SIMPLE_GRANT_AUTHORITY_PREFIX = "ROLE_";
static final String ROLES_KEY = "roles";
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
......@@ -54,6 +73,8 @@ public class SecurityConfiguration {
http.authorizeHttpRequests(requests -> requests
.requestMatchers(HttpMethod.GET, "/api/environment").permitAll()
.requestMatchers("/api/configuration").hasRole(UserRole.ADMIN_ADMIN)
.requestMatchers("/api/configuration/**").hasRole(UserRole.ADMIN_ADMIN)
.requestMatchers("/api").authenticated()
.requestMatchers("/api/**").authenticated()
.requestMatchers("/actuator").permitAll()
......@@ -67,9 +88,42 @@ public class SecurityConfiguration {
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
var jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(jwt -> List.of(() -> "ROLE_USER"));
jwtConverter.setJwtGrantedAuthoritiesConverter(
this::convertJwtToGrantedAuthorities);
jwtConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME);
return jwtConverter;
}
Set<GrantedAuthority> convertJwtToGrantedAuthorities(Jwt jwt) {
return getRolesFromJwt(jwt)
.stream()
.map(this::mapRoleStringToGrantedAuthority)
.collect(toSet());
}
private GrantedAuthority mapRoleStringToGrantedAuthority(String role) {
return new SimpleGrantedAuthority(SIMPLE_GRANT_AUTHORITY_PREFIX + role);
}
List<String> getRolesFromJwt(Jwt jwt) {
return Optional.ofNullable(jwt.getClaimAsMap(RESOURCE_ACCESS_KEY))
.flatMap(resourceAccessMap -> getMap(resourceAccessMap, oAuth2Properties.getResource()))
.flatMap(adminClientMap -> getList(adminClientMap, ROLES_KEY))
.orElse(Collections.emptyList());
}
@SuppressWarnings("unchecked")
private Optional<Map<String, Object>> getMap(Map<String, Object> map, String mapKey) {
return Optional.ofNullable(map.get(mapKey))
.filter(Map.class::isInstance)
.map(obj -> (Map<String, Object>) obj);
}
@SuppressWarnings("unchecked")
private Optional<List<String>> getList(Map<String, Object> map, String mapKey) {
return Optional.ofNullable(map.get(mapKey))
.filter(List.class::isInstance)
.map(obj -> (List<String>) obj);
}
}
package de.ozgcloud.admin.setting;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
class AlfaSettingDTO implements ApplicationSettingDTO {
@JsonProperty("ozgcloud.postfach.signatur")
@Builder.Default
private String signatur = "";
}
package de.ozgcloud.admin.setting;
interface ApplicationSettingDTO {
}
package de.ozgcloud.admin.settings;
package de.ozgcloud.admin.setting;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import lombok.RequiredArgsConstructor;
@Configuration
@RequiredArgsConstructor
public class DataRestConfiguration implements RepositoryRestConfigurer {
@Autowired
private SettingsValidator settingsValidator;
private final SettingValidator settingValidator;
@Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener v) {
v.addValidator("beforeSave", settingsValidator);
v.addValidator("beforeCreate", settingsValidator);
v.addValidator("beforeSave", settingValidator);
v.addValidator("beforeCreate", settingValidator);
}
}
\ No newline at end of file
......@@ -19,7 +19,7 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.admin.settings;
package de.ozgcloud.admin.setting;
import jakarta.validation.Valid;
......@@ -36,8 +36,8 @@ import lombok.extern.jackson.Jacksonized;
@Builder
@Getter
@Jacksonized
@Document(Settings.COLLECTION_NAME)
public class Settings {
@Document(Setting.COLLECTION_NAME)
public class Setting {
static final String COLLECTION_NAME = "settings";
@Id
......@@ -46,5 +46,5 @@ public class Settings {
private String name;
@Valid
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.EXTERNAL_PROPERTY, property = "name")
private SettingsBody settingsBody;
private SettingBody settingBody;
}
package de.ozgcloud.admin.settings;
package de.ozgcloud.admin.setting;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import de.ozgcloud.admin.settings.postfach.Postfach;
import de.ozgcloud.admin.setting.postfach.PostfachSettingBody;
@JsonSubTypes({
@Type(value = Postfach.class, name = "Postfach")
@Type(value = PostfachSettingBody.class, name = "Postfach")
})
public interface SettingsBody {
public interface SettingBody {
}
......@@ -19,13 +19,13 @@
* Die sprachspezifischen Genehmigungen und Beschränkungen
* unter der Lizenz sind dem Lizenztext zu entnehmen.
*/
package de.ozgcloud.admin.settings;
package de.ozgcloud.admin.setting;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SettingsConstants {
public class SettingConstants {
static final String REL = "settings";
......
package de.ozgcloud.admin.setting;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.cloud.config.environment.PropertySource;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Component
public class SettingEnvironmentRepository implements EnvironmentRepository {
private final SettingService settingService;
private final ObjectMapper objectMapper;
private static final String ALFA = "Alfa";
private static final String VORGANG_MANAGER = "OzgCloud_VorgangManager";
@Override
public Environment findOne(String application, String profile, String label) {
return buildEnvironment(application, findAnwendungSettingDTO(application));
}
Optional<ApplicationSettingDTO> findAnwendungSettingDTO(String application) {
switch (application) {
case ALFA:
return Optional.of(settingService.getAlfaSettingDTO());
case VORGANG_MANAGER:
return Optional.of(settingService.getVorgangManagerSettingDTO());
default:
return Optional.empty();
}
}
Environment buildEnvironment(String application, Optional<ApplicationSettingDTO> settingDTO) {
var environment = new Environment(application);
settingDTO.ifPresent(setting -> environment.add(transformToPropertySource(setting)));
return environment;
}
PropertySource transformToPropertySource(ApplicationSettingDTO settingDTO) {
return new PropertySource(StringUtils.EMPTY, objectMapper.convertValue(settingDTO, new TypeReference<Map<String, Object>>() {
}));
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment