/*
* Copyright 2016 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.exception.DatabaseException;
import liquibase.statement.core.InsertStatement;
import liquibase.statement.core.UpdateStatement;
import liquibase.structure.core.Table;
import org.keycloak.Config;
import org.keycloak.migration.MigrationProvider;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClaimMask;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JpaUpdate1_2_0_Beta1 extends CustomKeycloakTask {
private String realmTableName;
@Override
protected String getTaskId() {
return "Update 1.2.0.Beta1";
}
@Override
protected void generateStatementsImpl() throws CustomChangeException {
realmTableName = database.correctObjectName("REALM", Table.class);
try {
convertSocialToIdFedRealms();
convertSocialToIdFedUsers();
addAccessCodeLoginTimeout();
addNewAdminRoles();
addDefaultProtocolMappers();
} catch (Exception e) {
throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
}
}
protected void convertSocialToIdFedRealms() throws SQLException, DatabaseException {
String identityProviderTableName = database.correctObjectName("IDENTITY_PROVIDER", Table.class);
String idpConfigTableName = database.correctObjectName("IDENTITY_PROVIDER_CONFIG", Table.class);
String realmSocialConfigTable = getTableName("REALM_SOCIAL_CONFIG");
String realmTableName = getTableName("REALM");
PreparedStatement statement = jdbcConnection.prepareStatement("select RSC.NAME, VALUE, REALM_ID, UPDATE_PROFILE_ON_SOC_LOGIN from " + realmSocialConfigTable + " RSC," + realmTableName +
" REALM where RSC.REALM_ID = REALM.ID ORDER BY RSC.REALM_ID, RSC.NAME");
try {
ResultSet resultSet = statement.executeQuery();
try {
boolean providerInProgress = false;
String socialProviderId = null;
String clientId = null;
String clientSecret;
String realmId = null;
boolean updateProfileOnSocialLogin = false;
boolean first = true;
while (resultSet.next()) {
if (first) {
confirmationMessage.append("Migrating social to identity providers: ");
first = false;
}
if (!providerInProgress) {
String key = resultSet.getString("NAME");
int keyIndex = key.indexOf(".key");
if (keyIndex == -1) {
throw new IllegalStateException("Can't parse the provider from column: " + key);
}
socialProviderId = key.substring(0, keyIndex);
clientId = resultSet.getString("VALUE");
realmId = resultSet.getString("REALM_ID");
updateProfileOnSocialLogin = resultSet.getBoolean("UPDATE_PROFILE_ON_SOC_LOGIN");
providerInProgress = true;
} else {
clientSecret = resultSet.getString("VALUE");
String internalId = KeycloakModelUtils.generateId();
InsertStatement idpInsert = new InsertStatement(null, null, identityProviderTableName)
.addColumnValue("INTERNAL_ID", internalId)
.addColumnValue("ENABLED", true)
.addColumnValue("PROVIDER_ALIAS", socialProviderId)
.addColumnValue("PROVIDER_ID", socialProviderId)
.addColumnValue("UPDATE_PROFILE_FIRST_LOGIN", updateProfileOnSocialLogin)
.addColumnValue("STORE_TOKEN", false)
.addColumnValue("AUTHENTICATE_BY_DEFAULT", false)
.addColumnValue("REALM_ID", realmId);
InsertStatement clientIdInsert = new InsertStatement(null, null, idpConfigTableName)
.addColumnValue("IDENTITY_PROVIDER_ID", internalId)
.addColumnValue("NAME", "clientId")
.addColumnValue("VALUE", clientId);
InsertStatement clientSecretInsert = new InsertStatement(null, null, idpConfigTableName)
.addColumnValue("IDENTITY_PROVIDER_ID", internalId)
.addColumnValue("NAME", "clientSecret")
.addColumnValue("VALUE", clientSecret);
statements.add(idpInsert);
statements.add(clientIdInsert);
statements.add(clientSecretInsert);
confirmationMessage.append(socialProviderId + " in realm " + realmId + ", ");
providerInProgress = false;
}
}
// It means that some provider where processed
if (!first) {
confirmationMessage.append(". ");
}
} finally {
resultSet.close();
}
} finally {
statement.close();
}
}
protected void convertSocialToIdFedUsers() throws SQLException, DatabaseException {
String federatedIdentityTableName = database.correctObjectName("FEDERATED_IDENTITY", Table.class);
PreparedStatement statement = jdbcConnection.prepareStatement("select REALM_ID, USER_ID, SOCIAL_PROVIDER, SOCIAL_USER_ID, SOCIAL_USERNAME from " + getTableName("USER_SOCIAL_LINK"));
try {
ResultSet resultSet = statement.executeQuery();
try {
int count = 0;
while (resultSet.next()) {
InsertStatement insert = new InsertStatement(null, null, federatedIdentityTableName)
.addColumnValue("REALM_ID", resultSet.getString("REALM_ID"))
.addColumnValue("USER_ID", resultSet.getString("USER_ID"))
.addColumnValue("IDENTITY_PROVIDER", resultSet.getString("SOCIAL_PROVIDER"))
.addColumnValue("FEDERATED_USER_ID", resultSet.getString("SOCIAL_USER_ID"))
.addColumnValue("FEDERATED_USERNAME", resultSet.getString("SOCIAL_USERNAME"));
count++;
statements.add(insert);
}
confirmationMessage.append("Updating " + count + " social links to federated identities. ");
} finally {
resultSet.close();
}
} finally {
statement.close();
}
}
protected void addAccessCodeLoginTimeout() {
UpdateStatement statement = new UpdateStatement(null, null, realmTableName)
.addNewColumnValue("LOGIN_LIFESPAN", 1800)
.setWhereClause("LOGIN_LIFESPAN IS NULL");
statements.add(statement);
confirmationMessage.append("Updated LOGIN_LIFESPAN of all realms to 1800 seconds. ");
}
private void addNewAdminRoles() throws SQLException, DatabaseException{
addNewMasterAdminRoles();
addNewRealmAdminRoles();
confirmationMessage.append("Adding new admin roles. ");
}
protected void addNewMasterAdminRoles() throws SQLException, DatabaseException {
// Retrieve ID of admin role of master realm
String adminRoleId = getAdminRoleId();
String masterRealmId = Config.getAdminRealm();
PreparedStatement statement = jdbcConnection.prepareStatement("select NAME from " + getTableName("REALM"));
try {
ResultSet resultSet = statement.executeQuery();
try {
while (resultSet.next()) {
String realmName = resultSet.getString("NAME");
String masterAdminAppName = realmName + "-realm";
PreparedStatement statement2 = jdbcConnection.prepareStatement("select ID from " + getTableName("CLIENT") + " where REALM_ID = ? AND NAME = ?");
statement2.setString(1, masterRealmId);
statement2.setString(2, masterAdminAppName);
try {
ResultSet resultSet2 = statement2.executeQuery();
try {
if (resultSet2.next()) {
String masterAdminAppId = resultSet2.getString("ID");
addAdminRole(AdminRoles.VIEW_IDENTITY_PROVIDERS, masterRealmId, masterAdminAppId, adminRoleId);
addAdminRole(AdminRoles.MANAGE_IDENTITY_PROVIDERS, masterRealmId, masterAdminAppId, adminRoleId);
} else {
throw new IllegalStateException("Couldn't find ID of '" + masterAdminAppName + "' application in 'master' realm. ");
}
} finally {
resultSet2.close();
}
} finally {
statement2.close();
}
}
} finally {
resultSet.close();
}
} finally {
statement.close();
}
}
private String getAdminRoleId() throws SQLException, DatabaseException {
PreparedStatement statement = jdbcConnection.prepareStatement("select ID from " + getTableName("KEYCLOAK_ROLE") + " where NAME = ? AND REALM = ?");
statement.setString(1, AdminRoles.ADMIN);
statement.setString(2, Config.getAdminRealm());
try {
ResultSet resultSet = statement.executeQuery();
try {
if (resultSet.next()) {
return resultSet.getString("ID");
} else {
throw new IllegalStateException("Couldn't find ID of 'admin' role in 'master' realm");
}
} finally {
resultSet.close();
}
} finally {
statement.close();
}
}
protected void addNewRealmAdminRoles() throws SQLException, DatabaseException {
PreparedStatement statement = jdbcConnection.prepareStatement("select CLIENT.ID REALM_ADMIN_APP_ID, CLIENT.REALM_ID REALM_ID, KEYCLOAK_ROLE.ID ADMIN_ROLE_ID from " +
getTableName("CLIENT") + " CLIENT," + getTableName("KEYCLOAK_ROLE") + " KEYCLOAK_ROLE where KEYCLOAK_ROLE.APPLICATION = CLIENT.ID AND CLIENT.NAME = 'realm-management' AND KEYCLOAK_ROLE.NAME = ?");
statement.setString(1, AdminRoles.REALM_ADMIN);
try {
ResultSet resultSet = statement.executeQuery();
try {
while (resultSet.next()) {
String realmAdminAppId = resultSet.getString("REALM_ADMIN_APP_ID");
String realmId = resultSet.getString("REALM_ID");
String adminRoleId = resultSet.getString("ADMIN_ROLE_ID");
addAdminRole(AdminRoles.VIEW_IDENTITY_PROVIDERS, realmId, realmAdminAppId, adminRoleId);
addAdminRole(AdminRoles.MANAGE_IDENTITY_PROVIDERS, realmId, realmAdminAppId, adminRoleId);
}
} finally {
resultSet.close();
}
} finally {
statement.close();
}
}
private void addAdminRole(String roleName, String realmId, String applicationId, String realmAdminAppRoleId) {
String roleTableName = database.correctObjectName("KEYCLOAK_ROLE", Table.class);
String compositeRoleTableName = database.correctObjectName("COMPOSITE_ROLE", Table.class);
String newRoleId = KeycloakModelUtils.generateId();
InsertStatement insertRole = new InsertStatement(null, null, roleTableName)
.addColumnValue("ID", newRoleId)
.addColumnValue("APP_REALM_CONSTRAINT", applicationId)
.addColumnValue("APPLICATION_ROLE", true)
.addColumnValue("NAME", roleName)
.addColumnValue("REALM_ID", realmId)
.addColumnValue("APPLICATION", applicationId);
// Add newly created role to the composite roles of 'realm-admin' role
InsertStatement insertCompRole = new InsertStatement(null, null, compositeRoleTableName)
.addColumnValue("COMPOSITE", realmAdminAppRoleId)
.addColumnValue("CHILD_ROLE", newRoleId);
statements.add(insertRole);
statements.add(insertCompRole);
}
protected void addDefaultProtocolMappers() throws SQLException, DatabaseException {
String protocolMapperTableName = database.correctObjectName("PROTOCOL_MAPPER", Table.class);
String protocolMapperCfgTableName = database.correctObjectName("PROTOCOL_MAPPER_CONFIG", Table.class);
PreparedStatement statement = jdbcConnection.prepareStatement("select ID, NAME, ALLOWED_CLAIMS_MASK from " + getTableName("CLIENT"));
try {
ResultSet resultSet = statement.executeQuery();
try {
boolean first = true;
while (resultSet.next()) {
if (first) {
confirmationMessage.append("Migrating claimsMask to protocol mappers for clients: ");
first = false;
}
Object acmObj = resultSet.getObject("ALLOWED_CLAIMS_MASK");
long mask = (acmObj != null) ? ((Number) acmObj).longValue() : ClaimMask.ALL;
MigrationProvider migrationProvider = this.kcSession.getProvider(MigrationProvider.class);
List<ProtocolMapperRepresentation> protocolMappers = migrationProvider.getMappersForClaimMask(mask);
for (ProtocolMapperRepresentation protocolMapper : protocolMappers) {
String mapperId = KeycloakModelUtils.generateId();
InsertStatement insert = new InsertStatement(null, null, protocolMapperTableName)
.addColumnValue("ID", mapperId)
.addColumnValue("PROTOCOL", protocolMapper.getProtocol())
.addColumnValue("NAME", protocolMapper.getName())
.addColumnValue("CONSENT_REQUIRED", protocolMapper.isConsentRequired())
.addColumnValue("CONSENT_TEXT", protocolMapper.getConsentText())
.addColumnValue("PROTOCOL_MAPPER_NAME", protocolMapper.getProtocolMapper())
.addColumnValue("CLIENT_ID", resultSet.getString("ID"));
statements.add(insert);
for (Map.Entry<String, String> cfgEntry : protocolMapper.getConfig().entrySet()) {
InsertStatement cfgInsert = new InsertStatement(null, null, protocolMapperCfgTableName)
.addColumnValue("PROTOCOL_MAPPER_ID", mapperId)
.addColumnValue("NAME", cfgEntry.getKey())
.addColumnValue("VALUE", cfgEntry.getValue());
statements.add(cfgInsert);
}
}
confirmationMessage.append(resultSet.getString("NAME") + ", ");
}
// It means that some provider where processed
if (!first) {
confirmationMessage.append(". ");
}
} finally {
resultSet.close();
}
} finally {
statement.close();
}
}
}