Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[issue-945] Add support for import of message bundles #950

Merged
merged 11 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ jobs:
echo "JAVAX_PROFILE=-Ppre-keycloak22" >> $GITHUB_ENV
echo "ADJUSTED_RESTEASY_VERSION=-Dresteasy.version=4.7.7.Final" >> $GITHUB_ENV

- name: Adapt sources for Keycloak versions < 19.0.0
if: ${{ matrix.env.KEYCLOAK_VERSION < '19.0.0' }}
run: |
echo "JAVAX_PROFILE=-Ppre-keycloak19" >> $GITHUB_ENV

- name: Build & Test
run: ./mvnw ${MAVEN_CLI_OPTS} -Dkeycloak.version=${{ matrix.env.KEYCLOAK_VERSION }} ${ADJUSTED_RESTEASY_VERSION} clean verify -Pcoverage ${JAVAX_PROFILE}

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
- Added support for managing message bundles

## [5.11.1] - 2024-03-12
- fixed github actions workflow permissions
Expand Down
16 changes: 15 additions & 1 deletion contrib/example-config/moped.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"id": "moped",
"realm": "moped",
"displayName": "MOPED Realm",
"internationalizationEnabled": true,
"supportedLocales": [
"en", "de"
],
"authenticationFlows": [
{
"alias": "my docker auth",
Expand Down Expand Up @@ -988,5 +992,15 @@
"require.password.update.after.registration": "false"
}
}
]
],
"messageBundles": {
"de": {
"hello": "Hallo",
"world": "Welt!"
},
"en": {
"hello": "Hello",
"world": "World!"
}
}
}
1 change: 1 addition & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
| Synchronize user federation | 3.5.0 | Synchronize the user federation defined on the realm configuration |
| Synchronize user profile | 5.4.0 | Synchronize the user profile configuration defined on the realm configuration |
| Synchronize client-policies | 5.6.0 | Synchronize the client-policies (clientProfiles and clientPolicies) while updating realms |
| Synchronize message bundles | 6.0.0 | Synchronize message bundles defined on the realm configuration |

# Specificities

Expand Down
1 change: 1 addition & 0 deletions docs/MANAGED.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ groups will be deleted. If you define `groups` but set an empty array, keycloak
| Clients Authorization Resources | The 'Default Resource' is always included. | `client-authorization-resources` |
| Clients Authorization Policies | - | `client-authorization-policies` |
| Clients Authorization Scopes | - | `client-authorization-scopes` |
| Message Bundles | Only message bundles imported with config-cli will be managed/deleted. | `message-bundles` |

## Disable deletion of managed entities

Expand Down
88 changes: 88 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,83 @@
</build>

<profiles>
<profile>
<id>pre-keycloak19</id>
<build>
<plugins>
<plugin>
<groupId>com.coderplus.maven.plugins</groupId>
<artifactId>copy-rename-maven-plugin</artifactId>
<version>1.0.1</version>
<executions>
<execution>
<id>replace-localizationutil-with-legacy</id>
<phase>generate-sources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<sourceFile>${project.basedir}/src/main/java/de/adorsys/keycloak/config/util/LocalizationUtil.java.legacy-pre-19</sourceFile>
<destinationFile>${project.basedir}/src/main/java/de/adorsys/keycloak/config/util/LocalizationUtil.java</destinationFile>
</configuration>
</execution>
<execution>
<id>replace-subgrouputil-with-legacy</id>
<phase>generate-sources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<sourceFile>${project.basedir}/src/test/java/de/adorsys/keycloak/config/test/util/SubGroupUtil.java.legacy</sourceFile>
<destinationFile>${project.basedir}/src/test/java/de/adorsys/keycloak/config/test/util/SubGroupUtil.java</destinationFile>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId>
<version>${maven-replacer.version}</version>
<executions>
<execution>
<id>replace-pre-keycloak22</id>
<phase>generate-sources</phase>
<goals>
<goal>replace</goal>
</goals>
</execution>
</executions>
<configuration>
<basedir>
${project.basedir}/src
</basedir>
<includes>
<include>**/*.java</include>
</includes>
<replacements>
<replacement>
<token>import jakarta</token>
<value>import javax</value>
</replacement>
<replacement>
<token>;
import org.keycloak.representations.userprofile.config.UPConfig;</token>
<value>;</value>
</replacement>
<replacement>
<token>userProfileResource.update\(JsonUtil.readValue\(newUserProfileConfiguration, UPConfig.class\)\);</token>
<value>userProfileResource.update(newUserProfileConfiguration);</value>
</replacement>
<replacement>
<token>return groupResource.getSubGroups\(null, null, false\);</token>
<value>return groupResource.toRepresentation().getSubGroups();</value>
</replacement>
</replacements>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>pre-keycloak22</id>
<build>
Expand All @@ -707,6 +784,17 @@
<artifactId>copy-rename-maven-plugin</artifactId>
<version>1.0.1</version>
<executions>
<execution>
<id>replace-localizationutil-with-legacy</id>
<phase>generate-sources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<sourceFile>${project.basedir}/src/main/java/de/adorsys/keycloak/config/util/LocalizationUtil.java.legacy</sourceFile>
<destinationFile>${project.basedir}/src/main/java/de/adorsys/keycloak/config/util/LocalizationUtil.java</destinationFile>
</configuration>
</execution>
<execution>
<id>replace-subgrouputil-with-legacy</id>
<phase>generate-sources</phase>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/de/adorsys/keycloak/config/model/RealmImport.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class RealmImport extends RealmRepresentation {

private Map<String, List<Map<String, Object>>> userProfile;

private Map<String, Map<String, String>> messageBundles;

private String checksum;

@Override
Expand All @@ -58,6 +60,16 @@ public void setUserProfile(Map<String, List<Map<String, Object>>> userProfile) {
this.userProfile = userProfile;
}

public Map<String, Map<String, String>> getMessageBundles() {
return messageBundles;
}

@SuppressWarnings("unused")
@JsonSetter("messageBundles")
public void setMessageBundles(Map<String, Map<String, String>> messageBundles) {
this.messageBundles = messageBundles;
}

public Map<String, List<Map<String, Object>>> getUserProfile() {
return userProfile;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ public static class ImportManagedProperties {
@NotNull
private final ImportManagedPropertiesValues clientAuthorizationScopes;

@NotNull
private final ImportManagedPropertiesValues messageBundles;

public ImportManagedProperties(ImportManagedPropertiesValues requiredAction, ImportManagedPropertiesValues group,
ImportManagedPropertiesValues clientScope, ImportManagedPropertiesValues scopeMapping,
ImportManagedPropertiesValues clientScopeMapping, ImportManagedPropertiesValues component,
Expand All @@ -164,7 +167,8 @@ public ImportManagedProperties(ImportManagedPropertiesValues requiredAction, Imp
ImportManagedPropertiesValues role, ImportManagedPropertiesValues client,
ImportManagedPropertiesValues clientAuthorizationResources,
ImportManagedPropertiesValues clientAuthorizationPolicies,
ImportManagedPropertiesValues clientAuthorizationScopes) {
ImportManagedPropertiesValues clientAuthorizationScopes,
ImportManagedPropertiesValues messageBundles) {
this.requiredAction = requiredAction;
this.group = group;
this.clientScope = clientScope;
Expand All @@ -180,6 +184,7 @@ public ImportManagedProperties(ImportManagedPropertiesValues requiredAction, Imp
this.clientAuthorizationResources = clientAuthorizationResources;
this.clientAuthorizationPolicies = clientAuthorizationPolicies;
this.clientAuthorizationScopes = clientAuthorizationScopes;
this.messageBundles = messageBundles;
}

public ImportManagedPropertiesValues getRequiredAction() {
Expand Down Expand Up @@ -242,6 +247,10 @@ public ImportManagedPropertiesValues getClientAuthorizationScopes() {
return clientAuthorizationScopes;
}

public ImportManagedPropertiesValues getMessageBundles() {
return messageBundles;
}

public enum ImportManagedPropertiesValues {
FULL, NO_DELETE
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*-
* ---license-start
* keycloak-config-cli
* ---
* Copyright (C) 2017 - 2021 adorsys GmbH & Co. KG @ https://adorsys.com
* ---
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ---license-end
*/

package de.adorsys.keycloak.config.service;

import de.adorsys.keycloak.config.model.RealmImport;
import de.adorsys.keycloak.config.properties.ImportConfigProperties;
import de.adorsys.keycloak.config.repository.RealmRepository;
import de.adorsys.keycloak.config.service.state.StateService;
import de.adorsys.keycloak.config.util.LocalizationUtil;
import org.keycloak.admin.client.resource.RealmLocalizationResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Creates and updates message bundles in your realm
*/
@Service
public class MessageBundleImportService {
private static final Logger logger = LoggerFactory.getLogger(MessageBundleImportService.class);

private final RealmRepository realmRepository;
private final ImportConfigProperties importConfigProperties;
private final StateService stateService;

@Autowired
public MessageBundleImportService(RealmRepository realmRepository, ImportConfigProperties importConfigProperties,
StateService stateService) {
this.realmRepository = realmRepository;
this.importConfigProperties = importConfigProperties;
this.stateService = stateService;
}

public void doImport(RealmImport realmImport) {
Map<String, Map<String, String>> messageBundles = realmImport.getMessageBundles();
if (messageBundles == null) return;

String realmName = realmImport.getRealm();
RealmLocalizationResource localizationResource = realmRepository.getResource(realmName).localization();
Set<Map.Entry<String, Map<String, String>>> locales = messageBundles.entrySet();

if (importConfigProperties.getManaged().getMessageBundles() == ImportConfigProperties
.ImportManagedProperties.ImportManagedPropertiesValues.FULL) {
deleteMessageBundlesMissingOnImport(realmName, realmImport.getMessageBundles());
}

for (Map.Entry<String, Map<String, String>> localeEntry : locales) {
String locale = localeEntry.getKey();
Map<String, String> newMessageBundles = localeEntry.getValue();
Map<String, String> oldMessageBundles = LocalizationUtil
.getRealmLocalizationTexts(localizationResource, locale);

localizationResource.createOrUpdateRealmLocalizationTexts(locale, newMessageBundles);

// clean up
if (oldMessageBundles != null
&& importConfigProperties.getManaged().getMessageBundles() == ImportConfigProperties
.ImportManagedProperties.ImportManagedPropertiesValues.FULL) {
for (String oldMessageBundleKey : oldMessageBundles.keySet()) {
if (!newMessageBundles.containsKey(oldMessageBundleKey)) {
localizationResource.deleteRealmLocalizationText(locale, oldMessageBundleKey);
logger.debug("Delete message bundle localization text with key '{}' for locale '{}' in realm '{}'",
oldMessageBundleKey, locale, realmName);
}
}
}
}
}

private void deleteMessageBundlesMissingOnImport(
String realmName,
Map<String, Map<String, String>> importedMessageBundles) {
if (importConfigProperties.getRemoteState().isEnabled()) {
// unknown message bundles are ignored always
List<String> messageBundlesInState = stateService.getMessageBundles();

Set<String> importMessageBundles = importedMessageBundles.keySet();

for (String messageBundle : messageBundlesInState) {
if (importMessageBundles.contains(messageBundle)) continue;

logger.debug("Delete message bundle '{}' in realm '{}'", messageBundle, realmName);
realmRepository.getResource(realmName).localization().deleteRealmLocalizationTexts(messageBundle);
}
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public class RealmImportService {
private final ClientAuthorizationImportService clientAuthorizationImportService;
private final ClientScopeMappingImportService clientScopeMappingImportService;
private final IdentityProviderImportService identityProviderImportService;
private final MessageBundleImportService messageBundleImportService;

private final ImportConfigProperties importProperties;

Expand Down Expand Up @@ -109,6 +110,7 @@ public RealmImportService(
ClientAuthorizationImportService clientAuthorizationImportService,
ClientScopeMappingImportService clientScopeMappingImportService,
IdentityProviderImportService identityProviderImportService,
MessageBundleImportService messageBundleImportService,
ChecksumService checksumService,
StateService stateService) {
this.importProperties = importProperties;
Expand All @@ -130,6 +132,7 @@ public RealmImportService(
this.clientAuthorizationImportService = clientAuthorizationImportService;
this.clientScopeMappingImportService = clientScopeMappingImportService;
this.identityProviderImportService = identityProviderImportService;
this.messageBundleImportService = messageBundleImportService;
this.checksumService = checksumService;
this.stateService = stateService;
}
Expand Down Expand Up @@ -212,6 +215,7 @@ private void configureRealm(RealmImport realmImport, RealmRepresentation existin
scopeMappingImportService.doImport(realmImport);
clientScopeMappingImportService.doImport(realmImport);
clientScopeImportService.doRemoveOrphan(realmImport);
messageBundleImportService.doImport(realmImport);

stateService.doImport(realmImport);
checksumService.doImport(realmImport);
Expand Down
Loading
Loading