Skip to content

Commit

Permalink
Merge pull request #950 from antikalk/issue-945
Browse files Browse the repository at this point in the history
[issue-945] Add support for import of message bundles
  • Loading branch information
st3v0rr committed Mar 28, 2024
2 parents 312b066 + 4c19903 commit 80184b2
Show file tree
Hide file tree
Showing 22 changed files with 696 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/ci.yaml
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
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
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
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
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
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
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
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
@@ -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);
}
}
}


}
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

0 comments on commit 80184b2

Please sign in to comment.