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 @@ + + + + + + + + + + + + + + + + + + + + + + + +