diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml index 218a1aa7a5..d379ac8151 100755 --- a/docbook/reference/en/en-US/master.xml +++ b/docbook/reference/en/en-US/master.xml @@ -35,6 +35,7 @@ + @@ -126,6 +127,7 @@ This one is short &UserFederation; &Kerberos; &ExportImport; + &AdminRecovery; &ServerCache; &SAML; &SecurityVulnerabilities; diff --git a/docbook/reference/en/en-US/modules/admin-recovery.xml b/docbook/reference/en/en-US/modules/admin-recovery.xml new file mode 100755 index 0000000000..941284805f --- /dev/null +++ b/docbook/reference/en/en-US/modules/admin-recovery.xml @@ -0,0 +1,15 @@ + + Recovering the Master Admin User + + It is possible for the "admin" user in the master realm to become inoperable. This may be because it was + accidentally deleted, its role mappings were removed, or the password was simply forgotten. + + + To recover the master admin user, just start the server with the following system properties: + + Then you can log in to the master admin account with your temporary password. You will then be + prompted to immediately change this password. + + \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/offlineconfig/AdminRecovery.java b/services/src/main/java/org/keycloak/offlineconfig/AdminRecovery.java new file mode 100644 index 0000000000..cb775b1b5f --- /dev/null +++ b/services/src/main/java/org/keycloak/offlineconfig/AdminRecovery.java @@ -0,0 +1,90 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.offlineconfig; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; +import org.keycloak.services.managers.ApplianceBootstrap; + +/** + * Static utility class that performs recovery on the master admin account. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class AdminRecovery { + private static final Logger log = Logger.getLogger(AdminRecovery.class); + + public static final String RECOVER_ADMIN_ACCOUNT = "keycloak.recover-admin"; + public static final String TEMP_ADMIN_PASSWORD = "keycloak.temp-admin-password"; + + // Don't allow instances + private AdminRecovery() {} + + public static void recover(KeycloakSessionFactory sessionFactory) { + if (!needRecovery()) return; + + KeycloakSession session = sessionFactory.create(); + + session.getTransaction().begin(); + try { + doRecover(session, getTempAdminPassword()); + session.getTransaction().commit(); + log.info("*******************************"); + log.info("Recovered Master Admin account."); + log.info("*******************************"); + } finally { + session.close(); + System.clearProperty(RECOVER_ADMIN_ACCOUNT); + System.clearProperty(TEMP_ADMIN_PASSWORD); + } + } + + private static boolean needRecovery() { + String strNeedRecovery = System.getProperty(RECOVER_ADMIN_ACCOUNT, "false"); + return Boolean.parseBoolean(strNeedRecovery); + } + + private static String getTempAdminPassword() { + String tempAdminPassword = System.getProperty(TEMP_ADMIN_PASSWORD); + if ((tempAdminPassword == null) || tempAdminPassword.isEmpty()) { + throw new OfflineConfigException("Must provide temporary admin password to recover admin account."); + } + return tempAdminPassword; + } + + private static void doRecover(KeycloakSession session, String tempAdminPassword) { + RealmProvider realmProvider = session.realms(); + UserProvider userProvider = session.users(); + + String adminRealmName = Config.getAdminRealm(); + RealmModel realm = realmProvider.getRealmByName(adminRealmName); + UserModel adminUser = userProvider.getUserByUsername("admin", realm); + + if (adminUser == null) { + adminUser = userProvider.addUser(realm, "admin"); + } + + ApplianceBootstrap.setupAdminUser(session, realm, adminUser, tempAdminPassword); + } +} diff --git a/services/src/main/java/org/keycloak/offlineconfig/OfflineConfigException.java b/services/src/main/java/org/keycloak/offlineconfig/OfflineConfigException.java new file mode 100644 index 0000000000..09a4a5ca6d --- /dev/null +++ b/services/src/main/java/org/keycloak/offlineconfig/OfflineConfigException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.offlineconfig; + +/** + * Runtime exception thrown when an offline configuration fails. Offline + * configuration is defined as any configuration done before the Keycloak Server + * starts accepting requests. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class OfflineConfigException extends IllegalStateException { + + public OfflineConfigException(String msg) { + super(msg); + } +} diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index 8760ff01b3..7510572c79 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -61,11 +61,15 @@ public class ApplianceBootstrap { KeycloakModelUtils.generateRealmKeys(realm); UserModel adminUser = session.users().addUser(realm, "admin"); + setupAdminUser(session, realm, adminUser, "admin"); + } + + public static void setupAdminUser(KeycloakSession session, RealmModel realm, UserModel adminUser, String password) { adminUser.setEnabled(true); - UserCredentialModel password = new UserCredentialModel(); - password.setType(UserCredentialModel.PASSWORD); - password.setValue("admin"); - session.users().updateCredential(realm, adminUser, password); + UserCredentialModel usrCredModel = new UserCredentialModel(); + usrCredModel.setType(UserCredentialModel.PASSWORD); + usrCredModel.setValue(password); + session.users().updateCredential(realm, adminUser, usrCredModel); adminUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); RoleModel adminRole = realm.getRole(AdminRoles.ADMIN); diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index e4c821cd30..0e32fe8982 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -42,6 +42,7 @@ import java.util.HashSet; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; +import org.keycloak.offlineconfig.AdminRecovery; /** * @author Bill Burke @@ -88,6 +89,7 @@ public class KeycloakApplication extends Application { importRealms(context); migrateModel(); + AdminRecovery.recover(sessionFactory); setupScheduledTasks(sessionFactory); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java new file mode 100644 index 0000000000..7e070dd607 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.testsuite.offlineconfig; + +import org.junit.After; +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialValueModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.offlineconfig.AdminRecovery; +import org.keycloak.offlineconfig.OfflineConfigException; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.WebRule; + +/** + * Test the AdminRecovery class. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc. + */ +public class AdminRecoveryTest { + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(); + + @Rule + public WebRule webRule = new WebRule(this); + + // Verifies that system properties were cleared at the end of recovery + @After + public void verifySysPropsCleared() { + Assert.assertNull(System.getProperty(AdminRecovery.RECOVER_ADMIN_ACCOUNT)); + Assert.assertNull(System.getProperty(AdminRecovery.TEMP_ADMIN_PASSWORD)); + } + + @Test + public void testAdminDeletedRecovery() { + KeycloakSession session = keycloakRule.startSession(); + RealmModel masterRealm = session.realms().getRealmByName("master"); + UserModel adminUser = session.users().getUserByUsername("admin", masterRealm); + session.users().removeUser(masterRealm, adminUser); + adminUser = session.users().getUserByUsername("admin", masterRealm); + keycloakRule.stopSession(session, true); + + Assert.assertNull(adminUser); + + doAdminRecovery(session); + + session = keycloakRule.startSession(); + adminUser = session.users().getUserByUsername("admin", masterRealm); + Assert.assertNotNull(adminUser); + Assert.assertTrue(adminUser.getRequiredActions().contains(RequiredAction.UPDATE_PASSWORD.toString())); + } + + @Test + public void testAdminPasswordRecovery() { + KeycloakSession session = keycloakRule.startSession(); + RealmModel masterRealm = session.realms().getRealmByName("master"); + UserModel adminUser = session.users().getUserByUsername("admin", masterRealm); + UserCredentialValueModel password = adminUser.getCredentialsDirectly().get(0); + password.setValue("forgotten-password"); + adminUser.updateCredentialDirectly(password); + keycloakRule.stopSession(session, true); + + Assert.assertEquals("forgotten-password", getAdminPassword()); + + doAdminRecovery(session); + + Assert.assertNotEquals("forgotten-password", getAdminPassword()); + } + + @Test(expected = OfflineConfigException.class) + public void testAdminRecoveryWithoutPassword() { + KeycloakSession session = keycloakRule.startSession(); + System.setProperty(AdminRecovery.RECOVER_ADMIN_ACCOUNT, "true"); + AdminRecovery.recover(session.getKeycloakSessionFactory()); + } + + private void doAdminRecovery(KeycloakSession session) { + System.setProperty(AdminRecovery.RECOVER_ADMIN_ACCOUNT, "true"); + System.setProperty(AdminRecovery.TEMP_ADMIN_PASSWORD, "foo"); + AdminRecovery.recover(session.getKeycloakSessionFactory()); + } + + private String getAdminPassword() { + KeycloakSession session = keycloakRule.startSession(); + RealmModel masterRealm = session.realms().getRealmByName("master"); + UserModel adminUser = session.users().getUserByUsername("admin", masterRealm); + UserCredentialValueModel password = adminUser.getCredentialsDirectly().get(0); + keycloakRule.stopSession(session, true); + return password.getValue(); + } +}