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..74cfa3eb45 --- /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 + accentally 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 property: + + Then you can log in to the master admin account with the default password "admin". 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..0f384ce5d7 --- /dev/null +++ b/services/src/main/java/org/keycloak/offlineconfig/AdminRecovery.java @@ -0,0 +1,80 @@ +/* + * 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"; + + // 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); + session.getTransaction().commit(); + log.info("*******************************"); + log.info("Recovered Master Admin account."); + log.info("*******************************"); + } finally { + session.close(); + System.setProperty(AdminRecovery.RECOVER_ADMIN_ACCOUNT, "false"); + } + } + + private static boolean needRecovery() { + String strNeedRecovery = System.getProperty(RECOVER_ADMIN_ACCOUNT, "false"); + return Boolean.parseBoolean(strNeedRecovery); + } + + private static void doRecover(KeycloakSession session) { + 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); + } +} 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..fbd6ea5309 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -61,6 +61,10 @@ public class ApplianceBootstrap { KeycloakModelUtils.generateRealmKeys(realm); UserModel adminUser = session.users().addUser(realm, "admin"); + setupAdminUser(session, realm, adminUser); + } + + public static void setupAdminUser(KeycloakSession session, RealmModel realm, UserModel adminUser) { adminUser.setEnabled(true); UserCredentialModel password = new UserCredentialModel(); password.setType(UserCredentialModel.PASSWORD); 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..b8aebbb2df --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java @@ -0,0 +1,94 @@ +/* + * 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.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.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); + + @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()); + } + + private void doAdminRecovery(KeycloakSession session) { + System.setProperty(AdminRecovery.RECOVER_ADMIN_ACCOUNT, "true"); + 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(); + } +}