Merge pull request #1335 from ssilvert/keycloak-config

KEYCLOAK-1404: Need recovery mechanism for master admin user
This commit is contained in:
Stian Thorgersen 2015-06-09 07:02:47 +01:00
commit c61c05be51
7 changed files with 260 additions and 4 deletions

View file

@ -35,6 +35,7 @@
<!ENTITY UserFederation SYSTEM "modules/user-federation.xml"> <!ENTITY UserFederation SYSTEM "modules/user-federation.xml">
<!ENTITY Kerberos SYSTEM "modules/kerberos.xml"> <!ENTITY Kerberos SYSTEM "modules/kerberos.xml">
<!ENTITY ExportImport SYSTEM "modules/export-import.xml"> <!ENTITY ExportImport SYSTEM "modules/export-import.xml">
<!ENTITY AdminRecovery SYSTEM "modules/admin-recovery.xml">
<!ENTITY ServerCache SYSTEM "modules/cache.xml"> <!ENTITY ServerCache SYSTEM "modules/cache.xml">
<!ENTITY SecurityVulnerabilities SYSTEM "modules/security-vulnerabilities.xml"> <!ENTITY SecurityVulnerabilities SYSTEM "modules/security-vulnerabilities.xml">
<!ENTITY Clustering SYSTEM "modules/clustering.xml"> <!ENTITY Clustering SYSTEM "modules/clustering.xml">
@ -126,6 +127,7 @@ This one is short
&UserFederation; &UserFederation;
&Kerberos; &Kerberos;
&ExportImport; &ExportImport;
&AdminRecovery;
&ServerCache; &ServerCache;
&SAML; &SAML;
&SecurityVulnerabilities; &SecurityVulnerabilities;

View file

@ -0,0 +1,15 @@
<chapter id="admin-recovery">
<title>Recovering the Master Admin User</title>
<para>
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.
</para>
<para>
To recover the master admin user, just start the server with the following system properties:
<programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.recover-admin=true -Dkeycloak.temp-admin-password=temppassword
]]></programlisting>
Then you can log in to the master admin account with your temporary password. You will then be
prompted to immediately change this password.
</para>
</chapter>

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -61,11 +61,15 @@ public class ApplianceBootstrap {
KeycloakModelUtils.generateRealmKeys(realm); KeycloakModelUtils.generateRealmKeys(realm);
UserModel adminUser = session.users().addUser(realm, "admin"); 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); adminUser.setEnabled(true);
UserCredentialModel password = new UserCredentialModel(); UserCredentialModel usrCredModel = new UserCredentialModel();
password.setType(UserCredentialModel.PASSWORD); usrCredModel.setType(UserCredentialModel.PASSWORD);
password.setValue("admin"); usrCredModel.setValue(password);
session.users().updateCredential(realm, adminUser, password); session.users().updateCredential(realm, adminUser, usrCredModel);
adminUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); adminUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
RoleModel adminRole = realm.getRole(AdminRoles.ADMIN); RoleModel adminRole = realm.getRole(AdminRoles.ADMIN);

View file

@ -42,6 +42,7 @@ import java.util.HashSet;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import org.keycloak.offlineconfig.AdminRecovery;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -88,6 +89,7 @@ public class KeycloakApplication extends Application {
importRealms(context); importRealms(context);
migrateModel(); migrateModel();
AdminRecovery.recover(sessionFactory);
setupScheduledTasks(sessionFactory); setupScheduledTasks(sessionFactory);
} }

View file

@ -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();
}
}