Prevent duplicate keys in message properties for themes (#37179)

Closes #33357

Signed-off-by: Alexander Schwartz <alexander.schwartz@gmx.net>
This commit is contained in:
Alexander Schwartz 2025-02-13 09:38:31 +01:00 committed by GitHub
parent ee74c28741
commit a0a5d0bcb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 339 additions and 30 deletions

View file

@ -50,6 +50,19 @@
</pluginManagement>
<plugins>
<plugin>
<groupId>org.keycloak</groupId>
<artifactId>theme-verifier-maven-plugin</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>verify-theme</id>
<goals>
<goal>verify-theme</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<!-- Clean child modules from parent, as we trigger the build here for parallelization. -->
<artifactId>maven-clean-plugin</artifactId>

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2025 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak</groupId>
<artifactId>theme-verifier-maven-plugin</artifactId>
<version>999.0.0-SNAPSHOT</version>
<name>Keycloak Theme verifier</name>
<packaging>maven-plugin</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven-plugin-tools.version>3.15.1</maven-plugin-tools.version>
<maven-plugin-api.version>3.9.9</maven-plugin-api.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>${maven-plugin-api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>${maven-plugin-tools.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>${maven-plugin-api.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.18.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,30 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.keycloak.themeverifier;
import org.apache.commons.io.filefilter.AbstractFileFilter;
import java.io.File;
public class MessagePropertiesFilter extends AbstractFileFilter {
public static MessagePropertiesFilter INSTANCE = new MessagePropertiesFilter();
@Override
public boolean accept(File file) {
return file.getName().startsWith("messages_") && file.getName().endsWith(".properties");
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.keycloak.themeverifier;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@Mojo(name = "verify-theme", defaultPhase = LifecyclePhase.INSTALL, threadSafe = true)
public class ThemeVerifierMojo extends AbstractMojo {
@Parameter(defaultValue = "${project}", readonly = true)
private MavenProject mavenProject;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
Iterator<Resource> resources = mavenProject.getResources().iterator();
List<String> messages = new ArrayList<>();
while (resources.hasNext()) {
Resource resource = resources.next();
File dir = new File(resource.getDirectory());
Iterator<File> fileIterator = FileUtils.iterateFiles(dir, MessagePropertiesFilter.INSTANCE, DirectoryFileFilter.INSTANCE);
while (fileIterator.hasNext()) {
File file = fileIterator.next();
messages.addAll(new VerifyMessageProperties(file).verify());
}
}
if (!messages.isEmpty()) {
throw new MojoFailureException("Validation errors: " + messages);
}
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.keycloak.themeverifier;
import org.apache.maven.plugin.MojoExecutionException;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
public class VerifyMessageProperties {
private final File file;
private List<String> messages;
public VerifyMessageProperties(File file) {
this.file = file;
}
public List<String> verify() throws MojoExecutionException {
messages = new ArrayList<>();
try {
String contents = Files.readString(file.toPath());
verifyNoDuplicateKeys(contents);
} catch (IOException e) {
throw new MojoExecutionException("Can not read file " + file, e);
}
return messages;
}
private void verifyNoDuplicateKeys(String contents) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new StringReader(contents));
String line;
HashSet<String> seenKeys = new HashSet<>();
HashSet<String> duplicateKeys = new HashSet<>();
while ((line = bufferedReader.readLine()) != null) {
if (line.startsWith("#") || line.isEmpty()) {
continue;
}
int split = line.indexOf("=");
if (split != -1) {
String key = line.substring(0, split).trim();
if (seenKeys.contains(key)) {
duplicateKeys.add(key);
} else {
seenKeys.add(key);
}
}
}
if (!duplicateKeys.isEmpty()) {
messages.add("Duplicate keys in file '" + file.getAbsolutePath() + "': " + duplicateKeys);
}
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.
*/
package org.keycloak.themeverifier;
import org.apache.maven.plugin.MojoExecutionException;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.net.URL;
import java.util.List;
class VerifyMessagePropertiesTest {
@Test
void verifyDuplicateKeysDetected() throws MojoExecutionException {
List<String> verify = getFile("duplicate_keys.properties").verify();
MatcherAssert.assertThat(verify, Matchers.contains(Matchers.containsString("Duplicate keys in file")));
}
private static VerifyMessageProperties getFile(String fixture) {
URL resource = VerifyMessageProperties.class.getResource("/" + fixture);
if (resource == null) {
throw new RuntimeException("Resource not found: " + fixture);
}
return new VerifyMessageProperties(new File(resource.getFile()));
}
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# 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.
#
key=value
key=value

View file

@ -291,6 +291,7 @@
<module>federation</module>
<module>services</module>
<module>themes</module>
<module>misc/theme-verifier</module>
<module>model</module>
<module>util</module>
<module>rest</module>

View file

@ -33,6 +33,21 @@
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.keycloak</groupId>
<artifactId>theme-verifier-maven-plugin</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>verify-theme</id>
<goals>
<goal>verify-theme</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>

View file

@ -289,31 +289,23 @@ authenticatorBackupCodesSetupTitle=Backup Kode Opsætning
realmName=Rige
doDownload=Download
doPrint=Print
doCopy=Kopier
generateNewBackupCodes=Generer Nye Backup Koder
backtoAuthenticatorPage=Tilbage til Authenticator siden
#Resources
resources=Ressourcer
myResources=Mine Ressourcer
sharedwithMe=Delt med mig
share=Del
resource=Ressource
application=Applikation
date=Dato
sharedwith=Delt med
owner=Ejer
accessPermissions=Adgangstilladelser
permissionRequests=Rettigheds forespørgsler
approve=Godkend
approveAll=Godkend alle
sharedwith=Delt med
people=Folk
perPage=per side
currentPage=Nuværende Side
sharetheResource=Del Ressourcen
user=Bruger
group=Gruppe
selectPermission=Vælg tilladelse
addPeople=Tilføj folk at dele ressourcen med

View file

@ -323,7 +323,6 @@ backupcodesIntroMessage=Jos menetät pääsyn puhelimeesi, voit silti kirjautua
realmName=Realm
doDownload=Lataa
doPrint=Tulosta
doCopy=Copy
backupCodesTips-1=Jokaisen varmuuskoodin voi käyttää yhden kerran.
backupCodesTips-2=Nämä koodit on luotu
generateNewBackupCodes=Luo uudet varmuuskoodit

View file

@ -155,7 +155,6 @@ backTo=Назад в {0}
date=Дата
event=Событие
ip=IP
client=Клиент
clients=Клиенты
details=Детали
started=Начата

View file

@ -477,7 +477,6 @@ webauthn-registration-init-label-prompt=등록한 패스키의 라벨을 입력
# WebAuthn Error
webauthn-error-title=패스키 오류
webauthn-error-registration=
webauthn-error-registration=패스키 등록에 실패했습니다.<br/> {0}
webauthn-error-api-get=패스키로 인증하는 데 실패했습니다.<br/> {0}
webauthn-error-different-user=첫 번째 인증된 사용자가 패스키로 인증된 사용자가 아닙니다.

View file

@ -247,24 +247,6 @@ notMatchPasswordMessage=Şifreler eşleşmiyor.
error-invalid-value=Geçersiz değer.
error-invalid-blank=Lütfen bir değer sağlayın.
error-empty=Lütfen bir değer sağlayın.
error-invalid-length=Uzunluk {1} ile {2} arasında olmalıdır.
error-invalid-length-too-short=Minimum uzunluk {1}.
error-invalid-length-too-long=Maksimum uzunluk {2}.
error-invalid-email=Geçersiz e-posta adresi.
error-invalid-number=Geçersiz numara.
error-number-out-of-range=Numara {1} ile {2} arasında olmalıdır.
error-number-out-of-range-too-small=Numara en az {1} değerinde olmalıdır.
error-number-out-of-range-too-big=Numara en fazla {2} değerinde olmalıdır.
error-pattern-no-match=Geçersiz değer.
error-invalid-uri=Geçersiz URL.
error-invalid-uri-scheme=Geçersiz URL şeması.
error-invalid-uri-fragment=Geçersiz URL parçası.
error-user-attribute-required=Bu alanı belirtiniz.
error-invalid-date=Geçersiz tarih.
error-user-attribute-read-only=Bu alan sadece okunabilir.
error-username-invalid-character=Değer geçersiz karakter içeriyor.
error-person-name-invalid-character=Değer geçersiz karakter içeriyor.
error-reset-otp-missing-id=Lütfen bir OTP yapılandırması seçin.
invalidPasswordExistingMessage=Mevcut şifre geçersiz.
invalidPasswordBlacklistedMessage=Geçersiz şifre: şifre kara listeye alındı.
@ -511,7 +493,6 @@ frontchannel-logout.message= Aşağıdaki uygulamalardan oturumunuzu kapatıyors
logoutConfirmTitle= Oturum kapatılıyor,
logoutConfirmHeader= Oturumunuzu kapatmak istiyor musunuz?,
doLogout= Oturumu kapat,
readOnlyUsernameMessage= Kullanıcı adınızı güncelleyemezsiniz çünkü sadece okunabilir durumda.,
error-invalid-multivalued-size= {0} özniteliği en az {1} ve en fazla {2} değer(ler)e sahip olmalıdır.,
shouldBeEqual= {0} {1} ile eşit olmalıdır,
shouldBeDifferent= {0} {1} ile farklı olmalıdır,