diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate25_0_0_ConsentConstraints.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate25_0_0_ConsentConstraints.java
new file mode 100644
index 0000000000..98d3d4d594
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate25_0_0_ConsentConstraints.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 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.connections.jpa.updater.liquibase.custom;
+
+import liquibase.exception.CustomChangeException;
+import liquibase.statement.core.RawSqlStatement;
+
+public class JpaUpdate25_0_0_ConsentConstraints extends CustomKeycloakTask {
+
+ @Override
+ protected void generateStatementsImpl() throws CustomChangeException {
+ final String userConsentClientScopeTable = getTableName("USER_CONSENT_CLIENT_SCOPE");
+ final String userConsentTable = getTableName("USER_CONSENT");
+ statements.add(new RawSqlStatement(
+ "DELETE FROM "+ userConsentClientScopeTable + " WHERE USER_CONSENT_ID IN (" +
+ " SELECT uc.ID FROM "+userConsentTable+" uc INNER JOIN (" +
+ " SELECT CLIENT_ID, USER_ID, MAX(LAST_UPDATED_DATE) AS MAX_UPDATED_DATE FROM " + userConsentTable +
+ " GROUP BY CLIENT_ID, USER_ID HAVING COUNT(*) > 1 ) max_dates ON uc.CLIENT_ID = max_dates.CLIENT_ID" +
+ " AND uc.USER_ID = max_dates.USER_ID AND uc.LAST_UPDATED_DATE = max_dates.MAX_UPDATED_DATE)"
+ ));
+ statements.add(new RawSqlStatement(
+ " DELETE FROM "+userConsentTable+" WHERE ID IN (" +
+ " SELECT uc.ID FROM "+userConsentTable+" uc INNER JOIN (" +
+ " SELECT CLIENT_ID, USER_ID, MAX(LAST_UPDATED_DATE) AS MAX_UPDATED_DATE" +
+ " FROM "+userConsentTable+" GROUP BY CLIENT_ID, USER_ID HAVING COUNT(*) > 1 )" +
+ " max_dates ON uc.CLIENT_ID = max_dates.CLIENT_ID" +
+ " AND uc.USER_ID = max_dates.USER_ID AND uc.LAST_UPDATED_DATE = max_dates.MAX_UPDATED_DATE )"
+ ));
+ statements.add(new RawSqlStatement(
+ " DELETE FROM "+ userConsentClientScopeTable + " WHERE USER_CONSENT_ID IN (" +
+ " SELECT uc.ID FROM "+userConsentTable+" uc INNER JOIN (" +
+ " SELECT CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID, MAX(LAST_UPDATED_DATE) AS MAX_UPDATED_DATE" +
+ " FROM "+userConsentTable+" GROUP BY CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID HAVING COUNT(*) > 1 )" +
+ " max_dates ON uc.CLIENT_STORAGE_PROVIDER = max_dates.CLIENT_STORAGE_PROVIDER" +
+ " AND uc.EXTERNAL_CLIENT_ID = max_dates.EXTERNAL_CLIENT_ID AND uc.USER_ID = max_dates.USER_ID AND uc.LAST_UPDATED_DATE = max_dates.MAX_UPDATED_DATE )"
+ ));
+ statements.add(new RawSqlStatement(
+ " DELETE FROM "+userConsentTable+" WHERE ID IN (" +
+ " SELECT uc.ID FROM "+userConsentTable+" uc INNER JOIN (" +
+ " SELECT CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID, MAX(LAST_UPDATED_DATE) AS MAX_UPDATED_DATE" +
+ " FROM "+userConsentTable+" GROUP BY CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID HAVING COUNT(*) > 1 )" +
+ " max_dates ON uc.CLIENT_STORAGE_PROVIDER = max_dates.CLIENT_STORAGE_PROVIDER" +
+ " AND uc.EXTERNAL_CLIENT_ID = max_dates.EXTERNAL_CLIENT_ID AND uc.USER_ID = max_dates.USER_ID AND uc.LAST_UPDATED_DATE = max_dates.MAX_UPDATED_DATE )"
+ ));
+ }
+
+ @Override
+ protected String getTaskId() {
+ return "Correct User Consent Unique Constraints for PostgreSQL and MariaDB";
+ }
+
+}
+
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate25_0_0_MySQL_ConsentConstraints.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate25_0_0_MySQL_ConsentConstraints.java
new file mode 100644
index 0000000000..22e8ebe4c8
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate25_0_0_MySQL_ConsentConstraints.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 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.connections.jpa.updater.liquibase.custom;
+
+import liquibase.exception.CustomChangeException;
+import liquibase.statement.core.DeleteStatement;
+import liquibase.statement.core.RawSqlStatement;
+import liquibase.structure.core.Table;
+
+public class JpaUpdate25_0_0_MySQL_ConsentConstraints extends CustomKeycloakTask {
+
+ @Override
+ protected void generateStatementsImpl() throws CustomChangeException {
+ final String userConsentClientScopeTable = getTableName("USER_CONSENT_CLIENT_SCOPE");
+ final String userConsentTable = getTableName("USER_CONSENT");
+ statements.add(new RawSqlStatement(
+ "DELETE FROM "+ userConsentClientScopeTable + " WHERE USER_CONSENT_ID IN (" +
+ " SELECT uc.ID FROM "+userConsentTable+" uc INNER JOIN (" +
+ " SELECT CLIENT_ID, USER_ID, MAX(LAST_UPDATED_DATE) AS MAX_UPDATED_DATE FROM " + userConsentTable +
+ " GROUP BY CLIENT_ID, USER_ID HAVING COUNT(*) > 1 ) max_dates ON uc.CLIENT_ID = max_dates.CLIENT_ID" +
+ " AND uc.USER_ID = max_dates.USER_ID AND uc.LAST_UPDATED_DATE = max_dates.MAX_UPDATED_DATE)"
+ ));
+ statements.add(new RawSqlStatement(
+ " CREATE TEMPORARY TABLE TEMP_USER_CONSENT_IDS" +
+ " AS SELECT uc.ID FROM "+userConsentTable+" uc INNER JOIN (" +
+ " SELECT CLIENT_ID, USER_ID, MAX(LAST_UPDATED_DATE) AS MAX_UPDATED_DATE" +
+ " FROM "+userConsentTable+" GROUP BY CLIENT_ID, USER_ID HAVING COUNT(*) > 1 )" +
+ " max_dates ON uc.CLIENT_ID = max_dates.CLIENT_ID" +
+ " AND uc.USER_ID = max_dates.USER_ID AND uc.LAST_UPDATED_DATE = max_dates.MAX_UPDATED_DATE"
+ ));
+ statements.add(new DeleteStatement(null, null, database.correctObjectName("USER_CONSENT", Table.class))
+ .setWhere("ID IN (SELECT ID FROM TEMP_USER_CONSENT_IDS)"));
+ statements.add(new RawSqlStatement("DROP TEMPORARY TABLE IF EXISTS TEMP_USER_CONSENT_IDS"));
+
+ statements.add(new RawSqlStatement(
+ " DELETE FROM "+ userConsentClientScopeTable + " WHERE USER_CONSENT_ID IN (" +
+ " SELECT uc.ID FROM "+userConsentTable+" uc INNER JOIN (" +
+ " SELECT CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID, MAX(LAST_UPDATED_DATE) AS MAX_UPDATED_DATE" +
+ " FROM "+userConsentTable+" GROUP BY CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID HAVING COUNT(*) > 1 )" +
+ " max_dates ON uc.CLIENT_STORAGE_PROVIDER = max_dates.CLIENT_STORAGE_PROVIDER" +
+ " AND uc.EXTERNAL_CLIENT_ID = max_dates.EXTERNAL_CLIENT_ID AND uc.USER_ID = max_dates.USER_ID AND uc.LAST_UPDATED_DATE = max_dates.MAX_UPDATED_DATE )"
+ ));
+ statements.add(new RawSqlStatement(
+ "CREATE TEMPORARY TABLE TEMP_USER_CONSENT_IDS2" +
+ " AS SELECT uc.ID FROM "+userConsentTable+" uc INNER JOIN (" +
+ " SELECT CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID, MAX(LAST_UPDATED_DATE) AS MAX_UPDATED_DATE" +
+ " FROM "+userConsentTable+" GROUP BY CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID HAVING COUNT(*) > 1 )" +
+ " max_dates ON uc.CLIENT_STORAGE_PROVIDER = max_dates.CLIENT_STORAGE_PROVIDER" +
+ " AND uc.EXTERNAL_CLIENT_ID = max_dates.EXTERNAL_CLIENT_ID AND uc.USER_ID = max_dates.USER_ID AND uc.LAST_UPDATED_DATE = max_dates.MAX_UPDATED_DATE;"
+ ));
+ statements.add(new DeleteStatement(null, null, database.correctObjectName("USER_CONSENT", Table.class))
+ .setWhere("ID IN (SELECT ID FROM TEMP_USER_CONSENT_IDS2)"));
+ statements.add(new RawSqlStatement("DROP TEMPORARY TABLE IF EXISTS TEMP_USER_CONSENT_IDS2"));
+ }
+
+ @Override
+ protected String getTaskId() {
+ return "Correct User Consent Unique Constraints for MySQL";
+ }
+
+}
+
+
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 30529556ba..6138049375 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -208,15 +208,10 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore {
@Override
public void addConsent(RealmModel realm, String userId, UserConsentModel consent) {
String clientId = consent.getClient().getId();
-
- UserConsentEntity consentEntity = getGrantedConsentEntity(userId, clientId, LockModeType.NONE);
- if (consentEntity != null) {
- throw new ModelDuplicateException("Consent already exists for client [" + clientId + "] and user [" + userId + "]");
- }
-
+
long currentTime = Time.currentTimeMillis();
- consentEntity = new UserConsentEntity();
+ UserConsentEntity consentEntity = new UserConsentEntity();
consentEntity.setId(KeycloakModelUtils.generateId());
consentEntity.setUser(em.getReference(UserEntity.class, userId));
StorageId clientStorageId = new StorageId(clientId);
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml
index 27a4738742..c815817bba 100644
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-25.0.0.xml
@@ -34,4 +34,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+