From 681251468337c87fc2b23022df8fa4ef66dccc80 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Thu, 4 Jun 2015 15:29:44 -0400 Subject: [PATCH 1/3] KEYCLOAK-1404: Need recovery mechanism for master admin user --- docbook/reference/en/en-US/master.xml | 2 + .../en/en-US/modules/admin-recovery.xml | 15 +++ .../keycloak/offlineconfig/AdminRecovery.java | 80 ++++++++++++++++ .../services/managers/ApplianceBootstrap.java | 4 + .../resources/KeycloakApplication.java | 2 + .../offlineconfig/AdminRecoveryTest.java | 94 +++++++++++++++++++ 6 files changed, 197 insertions(+) create mode 100755 docbook/reference/en/en-US/modules/admin-recovery.xml create mode 100644 services/src/main/java/org/keycloak/offlineconfig/AdminRecovery.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java 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(); + } +} From e48aafd588a05ec09f50caa4848235f61a53deba Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Fri, 5 Jun 2015 07:46:39 -0400 Subject: [PATCH 2/3] Fix spelling error. --- docbook/reference/en/en-US/modules/admin-recovery.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docbook/reference/en/en-US/modules/admin-recovery.xml b/docbook/reference/en/en-US/modules/admin-recovery.xml index 74cfa3eb45..e026cddecd 100755 --- a/docbook/reference/en/en-US/modules/admin-recovery.xml +++ b/docbook/reference/en/en-US/modules/admin-recovery.xml @@ -2,7 +2,7 @@ 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. + 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 property: From e977a363ef257d5707ac80a164bc03b4f33fc410 Mon Sep 17 00:00:00 2001 From: Stan Silvert Date: Mon, 8 Jun 2015 13:04:52 -0400 Subject: [PATCH 3/3] Require user to specify a temporary admin password to do admin recovery. --- .../en/en-US/modules/admin-recovery.xml | 6 ++-- .../keycloak/offlineconfig/AdminRecovery.java | 18 ++++++++--- .../offlineconfig/OfflineConfigException.java | 32 +++++++++++++++++++ .../services/managers/ApplianceBootstrap.java | 12 +++---- .../offlineconfig/AdminRecoveryTest.java | 17 ++++++++++ 5 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 services/src/main/java/org/keycloak/offlineconfig/OfflineConfigException.java diff --git a/docbook/reference/en/en-US/modules/admin-recovery.xml b/docbook/reference/en/en-US/modules/admin-recovery.xml index e026cddecd..941284805f 100755 --- a/docbook/reference/en/en-US/modules/admin-recovery.xml +++ b/docbook/reference/en/en-US/modules/admin-recovery.xml @@ -5,11 +5,11 @@ 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 property: + 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 the default password "admin". You will then be + 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 index 0f384ce5d7..cb775b1b5f 100644 --- a/services/src/main/java/org/keycloak/offlineconfig/AdminRecovery.java +++ b/services/src/main/java/org/keycloak/offlineconfig/AdminRecovery.java @@ -36,6 +36,7 @@ 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() {} @@ -47,14 +48,15 @@ public class AdminRecovery { session.getTransaction().begin(); try { - doRecover(session); + doRecover(session, getTempAdminPassword()); session.getTransaction().commit(); log.info("*******************************"); log.info("Recovered Master Admin account."); log.info("*******************************"); } finally { session.close(); - System.setProperty(AdminRecovery.RECOVER_ADMIN_ACCOUNT, "false"); + System.clearProperty(RECOVER_ADMIN_ACCOUNT); + System.clearProperty(TEMP_ADMIN_PASSWORD); } } @@ -63,7 +65,15 @@ public class AdminRecovery { return Boolean.parseBoolean(strNeedRecovery); } - private static void doRecover(KeycloakSession session) { + 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(); @@ -75,6 +85,6 @@ public class AdminRecovery { adminUser = userProvider.addUser(realm, "admin"); } - ApplianceBootstrap.setupAdminUser(session, realm, adminUser); + 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 fbd6ea5309..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,15 +61,15 @@ public class ApplianceBootstrap { KeycloakModelUtils.generateRealmKeys(realm); UserModel adminUser = session.users().addUser(realm, "admin"); - setupAdminUser(session, realm, adminUser); + setupAdminUser(session, realm, adminUser, "admin"); } - public static void setupAdminUser(KeycloakSession session, RealmModel realm, UserModel adminUser) { + 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/testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java index b8aebbb2df..7e070dd607 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/offlineconfig/AdminRecoveryTest.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.offlineconfig; +import org.junit.After; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; @@ -27,6 +28,7 @@ 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; @@ -42,6 +44,13 @@ public class AdminRecoveryTest { @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(); @@ -78,8 +87,16 @@ public class AdminRecoveryTest { 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()); }