diff --git a/core/src/main/java/hudson/model/UserPropertyListener.java b/core/src/main/java/hudson/model/UserPropertyListener.java new file mode 100644 index 000000000000..42f5d7cfb22f --- /dev/null +++ b/core/src/main/java/hudson/model/UserPropertyListener.java @@ -0,0 +1,111 @@ +/* + * The MIT License + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.model; + +import hudson.ExtensionList; +import hudson.ExtensionPoint; +import hudson.model.UserProperty; +import java.text.MessageFormat; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nonnull; + +/** + * Listener interface which all other user property-specific event-listeners make use of. + */ +public interface UserPropertyListener extends ExtensionPoint { + + static final Logger LOGGER = Logger.getLogger(UserPropertyListener.class.getName()); + + /** + * Fired when a new user property has been created. + * + * @param username the user + * @param value property that was newly created. + * + */ + default void onCreated(@Nonnull String username, @Nonnull UserProperty value) { + LOGGER.log(Level.FINE, MessageFormat.format("new {0} property created for user {1}", value.getClass().toString(), username)); + } + + /** + * Fired when a new user property has been created. + * + * @param username the user + * @param value property that was newly created. + * + */ + default void onCreated(@Nonnull String username, @Nonnull T value) { + LOGGER.log(Level.FINE, MessageFormat.format("new {0} property created for user {1}", value.toString(), username)); + } + + /** + * Fired when an existing user property has been changed. + * + * @param username the user + * @param oldValue old property of the user + * @param newValue new property of the user + * + */ + default void onChanged(@Nonnull String username, @Nonnull UserProperty oldValue, @Nonnull UserProperty newValue) { + LOGGER.log(Level.FINE, MessageFormat.format("{0} property changed for user {1}", oldValue.getClass().toString(), username)); + } + + /** + * Fired when an existing user property has been changed. + * + * @param username the user + * @param oldValue old property of the user + * @param newValue new property of the user + * + */ + default void onChanged(@Nonnull String username, @Nonnull T oldValue, @Nonnull T newValue) { + LOGGER.log(Level.FINE, MessageFormat.format("{0} property changed for user {1}", oldValue.toString(), username)); + } + + /** + * Fired when an existing user property has been removed or deleted. + * + * @param username the user + * @param value property that was removed. + * + */ + default void onDeleted(@Nonnull String username, @Nonnull UserProperty value) { + LOGGER.log(Level.FINE, MessageFormat.format("new {0} property created for user {1}", value.getClass().toString(), username)); + } + + /** + * Fired when an existing user property has been removed or deleted. + * + * @param username the user + * @param value property that was removed + * + */ + default void onDeleted(@Nonnull String username, @Nonnull T value) { + LOGGER.log(Level.FINE, MessageFormat.format("new {0} property created for user {1}", value.toString(), username)); + } + + static List all() { return ExtensionList.lookup(UserPropertyListener.class); } +} diff --git a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java index fe66e46becc9..b44aa851afac 100644 --- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java +++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java @@ -717,14 +717,21 @@ public Details newInstance(StaplerRequest req, JSONObject formData) throws FormE if(!Util.fixNull(pwd).equals(Util.fixNull(pwd2))) throw new FormException("Please confirm the password by typing it twice","user.password2"); + User user = Util.getNearestAncestorOfTypeOrThrow(req, User.class); + UserProperty p = user.getProperty(Details.class); + String currentUserHashedPassword = ((Details) p).getPassword(); + String data = Protector.unprotect(pwd); if(data!=null) { String prefix = Stapler.getCurrentRequest().getSession().getId() + ':'; if(data.startsWith(prefix)) + PasswordPropertyListener.fireOnChanged(user.getId(), Util.fixNull(currentUserHashedPassword), Util.fixNull(pwd)); return Details.fromHashedPassword(data.substring(prefix.length())); } - User user = Util.getNearestAncestorOfTypeOrThrow(req, User.class); + if (p != null) { + PasswordPropertyListener.fireOnChanged(user.getId(), Util.fixNull(currentUserHashedPassword), Util.fixNull(pwd)); + } // the UserSeedProperty is not touched by the configure page UserSeedProperty userSeedProperty = user.getProperty(UserSeedProperty.class); if (userSeedProperty != null) { diff --git a/core/src/main/java/hudson/security/PasswordPropertyListener.java b/core/src/main/java/hudson/security/PasswordPropertyListener.java new file mode 100644 index 000000000000..f1afb5db27b8 --- /dev/null +++ b/core/src/main/java/hudson/security/PasswordPropertyListener.java @@ -0,0 +1,59 @@ +/* + * The MIT License + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.security; + +import hudson.Extension; +import hudson.ExtensionList; +import hudson.model.UserPropertyListener; +import java.util.List; +import javax.annotation.Nonnull; + +import static hudson.security.HudsonPrivateSecurityRealm.PASSWORD_ENCODER; + +/** + * Listener notified of user password change events from the jenkins UI. + */ +@Extension +public class PasswordPropertyListener implements UserPropertyListener { + + /** + * @since TODO + * + * Fired when an existing user password property has been changed. + * + * @param username the user + * @param oldValue old password of the user + * @param newValue new password of the user + * + * **/ + static void fireOnChanged(@Nonnull String username, @Nonnull String oldValue, @Nonnull String newValue) { + if ((!oldValue.equals(newValue)) && (!PASSWORD_ENCODER.isPasswordValid(oldValue, newValue, null))) { + for (PasswordPropertyListener l : all()) { + l.onChanged(username, oldValue, newValue); + } + } + } + + static List all() { return ExtensionList.lookup(PasswordPropertyListener.class); } +} diff --git a/core/src/main/java/jenkins/security/ApiTokenProperty.java b/core/src/main/java/jenkins/security/ApiTokenProperty.java index 9cb01b62686e..99802b550a61 100644 --- a/core/src/main/java/jenkins/security/ApiTokenProperty.java +++ b/core/src/main/java/jenkins/security/ApiTokenProperty.java @@ -486,6 +486,9 @@ public HttpResponse doGenerateNewToken(@AncestorInPath User u, @QueryParameter S } ApiTokenStore.TokenUuidAndPlainValue tokenUuidAndPlainValue = p.tokenStore.generateNewToken(tokenName); + if ((p != null) && (tokenUuidAndPlainValue != null)) { + ApiTokenPropertyListener.fireOnCreated(u.getId(), p); + } u.save(); return HttpResponses.okJSON(new HashMap() {{ @@ -549,6 +552,7 @@ public HttpResponse doRevoke(@AncestorInPath User u, p.apiToken = null; } p.tokenStats.removeId(revoked.getUuid()); + ApiTokenPropertyListener.fireOnDeleted(u.getId(), p); } u.save(); diff --git a/core/src/main/java/jenkins/security/ApiTokenPropertyListener.java b/core/src/main/java/jenkins/security/ApiTokenPropertyListener.java new file mode 100644 index 000000000000..d4b66c99e900 --- /dev/null +++ b/core/src/main/java/jenkins/security/ApiTokenPropertyListener.java @@ -0,0 +1,74 @@ +/* + * The MIT License + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package jenkins.security; + +import hudson.Extension; +import hudson.ExtensionList; +import hudson.model.UserProperty; +import hudson.model.UserPropertyListener; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Listener notified of user api-token creation and deletion events from the jenkins UI. + */ +@Extension +public class ApiTokenPropertyListener implements UserPropertyListener { + + /** + * @since TODO + * + * Fired when an api token has been created. + * + * @param username the user + * @param value api token property of the user + * + * **/ + static void fireOnCreated(@Nonnull String username, @Nonnull UserProperty value) { + if (value instanceof ApiTokenProperty) { + for (ApiTokenPropertyListener l : all()) { + l.onCreated(username, value); + } + } + } + + /** + * @since TODO + * + * Fired when an api token has been revoked. + * + * @param username the user + * @param value api token property of the user + * + * **/ + static void fireOnDeleted(@Nonnull String username, @Nonnull UserProperty value) { + if (value instanceof ApiTokenProperty) { + for (ApiTokenPropertyListener l : all()) { + l.onDeleted(username, value); + } + } + } + + static List all() { return ExtensionList.lookup(ApiTokenPropertyListener.class); } +} diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java index 557dbbea4e1d..0a7c5280612d 100644 --- a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java +++ b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java @@ -26,6 +26,7 @@ import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.HttpMethod; +import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; @@ -33,12 +34,15 @@ import com.gargoylesoftware.htmlunit.xml.XmlPage; import hudson.ExtensionList; import hudson.model.User; +import hudson.model.UserProperty; import hudson.remoting.Base64; import static hudson.security.HudsonPrivateSecurityRealm.CLASSIC; import static hudson.security.HudsonPrivateSecurityRealm.PASSWORD_ENCODER; import hudson.security.pages.SignupPage; import java.io.UnsupportedEncodingException; import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -76,10 +80,12 @@ public class HudsonPrivateSecurityRealmTest { public JenkinsRule j = new JenkinsRule(); private SpySecurityListenerImpl spySecurityListener; + private SpyPasswordPropertyListenerImpl spyPasswordListener; @Before public void linkExtension() throws Exception { spySecurityListener = ExtensionList.lookup(SecurityListener.class).get(SpySecurityListenerImpl.class); + spyPasswordListener = ExtensionList.lookup(PasswordPropertyListener.class).get(SpyPasswordPropertyListenerImpl.class); } @Before @@ -342,6 +348,41 @@ public void userCreationWithHashedPasswords() throws Exception { assertTrue(spySecurityListener.createdUsers.get(0).equals("charlie_hashed")); } + @Issue("JENKINS-56008") + @Test + public void updateUserPassword() throws Exception { + HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null); + j.jenkins.setSecurityRealm(securityRealm); + WebClient wc = j.createWebClient(); + + spyPasswordListener.usersWithPasswordUpdate.clear(); + assertTrue(spyPasswordListener.usersWithPasswordUpdate.isEmpty()); + + // new user account creation + SignupPage signup = new SignupPage(wc.goTo("signup")); + signup.enterUsername("debbie"); + signup.enterPassword("debbie"); + signup.enterFullName(StringUtils.capitalize("debbie user")); + signup.enterEmail("debbie" + "@" + "debbie" + ".com"); + HtmlPage p = signup.submit(j); + + assertEquals(200, p.getWebResponse().getStatusCode()); + + // execute an http request to change a user's password from their config page + User debbie = User.getById("debbie", false); + URL configPage = wc.createCrumbedUrl(debbie.getUrl() + "/" + "configSubmit"); + String formData = "{\"fullName\": \"debbie user\", \"description\": \"\", \"userProperty3\": {\"primaryViewName\": \"\"}, \"userProperty5\": {\"user.password\": \"admin\", \"$redact\": [\"user.password\", \"user.password2\"], \"user.password2\": \"admin\"}, \"userProperty6\": {\"authorizedKeys\": \"\"}, \"userProperty8\": {\"insensitiveSearch\": true}, \"core:apply\": \"true\"}"; + + WebRequest request = new WebRequest(configPage, HttpMethod.POST); + request.setAdditionalHeader("Content-Type", "application/x-www-form-urlencoded"); + request.setRequestBody("json=" + URLEncoder.encode(formData, StandardCharsets.UTF_8.name())); + Page page = wc.getPage(request); + + // ensure user whose password was changed was in fact logged + assertEquals(200, page.getWebResponse().getStatusCode()); + assertTrue(spyPasswordListener.usersWithPasswordUpdate.get(0).equals("debbie")); + } + private void createFirstAccount(String login) throws Exception { assertNull(User.getById(login, false)); @@ -429,6 +470,16 @@ protected void loggedIn(@Nonnull String username) { protected void userCreated(@Nonnull String username) { createdUsers.add(username); } } + @TestExtension + public static class SpyPasswordPropertyListenerImpl extends PasswordPropertyListener { + private List usersWithPasswordUpdate = new ArrayList<>(); + + @Override + public void onChanged(@Nonnull String username, @Nonnull Object oldValue, @Nonnull Object newValue) { + usersWithPasswordUpdate.add(username); + } + } + @Issue("SECURITY-786") @Test public void controlCharacterAreNoMoreValid() throws Exception { diff --git a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java index fc9e43069d77..c6a6a6cdded9 100644 --- a/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java +++ b/test/src/test/java/jenkins/security/ApiTokenPropertyTest.java @@ -4,12 +4,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.xml.HasXPath.hasXPath; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; +import static org.junit.Assert.assertTrue; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.HttpMethod; @@ -18,33 +14,40 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.xml.XmlPage; +import hudson.ExtensionList; import hudson.Util; import hudson.model.Cause; import hudson.model.FreeStyleProject; import hudson.model.User; +import hudson.model.UserProperty; import hudson.security.ACL; import hudson.security.ACLContext; import java.net.HttpURLConnection; import java.net.URL; +import hudson.security.HudsonPrivateSecurityRealm; +import hudson.security.pages.SignupPage; import jenkins.model.Jenkins; import jenkins.security.apitoken.ApiTokenPropertyConfiguration; import jenkins.security.apitoken.ApiTokenStore; import jenkins.security.apitoken.ApiTokenTestHelper; import net.sf.json.JSONObject; +import org.apache.commons.lang.StringUtils; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.JenkinsRule; import org.jvnet.hudson.test.JenkinsRule.WebClient; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nonnull; import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.TestExtension; import org.jvnet.hudson.test.recipes.LocalData; /** @@ -52,6 +55,8 @@ */ public class ApiTokenPropertyTest { + private SpyApiTokenPropertyListenerImpl spyApiTokenListener; + @Rule public JenkinsRule j = new JenkinsRule(); @@ -59,6 +64,11 @@ public class ApiTokenPropertyTest { public void setupLegacyConfig(){ ApiTokenTestHelper.enableLegacyBehavior(); } + + @Before + public void linkExtension() throws Exception { + spyApiTokenListener = ExtensionList.lookup(ApiTokenPropertyListener.class).get(SpyApiTokenPropertyListenerImpl.class); + } /** * Tests the UI interaction and authentication. @@ -158,6 +168,69 @@ public void adminsShouldBeUnableToChangeTokensByDefault() throws Exception { Messages.ApiTokenProperty_ChangeToken_SuccessHidden(), "
" + res.getBody().asText() + "
"); } + @Issue("JENKINS-56170") + @Test + public void createUserTokenFromUi() throws Exception { + HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null); + j.jenkins.setSecurityRealm(securityRealm); + WebClient wc = j.createWebClient(); + + spyApiTokenListener.usersWithCreatedTokens.clear(); + assertTrue(spyApiTokenListener.usersWithCreatedTokens.isEmpty()); + + // new user account creation + SignupPage signup = new SignupPage(wc.goTo("signup")); + signup.enterUsername("charlie"); + signup.enterPassword("charlie"); + signup.enterFullName(StringUtils.capitalize("charlie user")); + signup.enterEmail("charlie" + "@" + "example.com"); + HtmlPage page = signup.submit(j); + + // execute an http request to create a new a user api token from their config page + User charlie = User.getById("charlie", false); + URL configPage = wc.createCrumbedUrl(charlie.getUrl() + "/" + "/descriptorByName/" + ApiTokenProperty.class.getName() + "/generateNewToken/?newTokenName=" + "charlie-token"); + Page p = wc.getPage(new WebRequest(configPage, HttpMethod.POST)); + + // ensure user whose new token was deleted was in fact logged + assertEquals(200, p.getWebResponse().getStatusCode()); + assertEquals("charlie", spyApiTokenListener.usersWithCreatedTokens.get(0)); + } + + @Issue("JENKINS-56170") + @Test + public void revokeUserTokenFromUi() throws Exception { + HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null); + j.jenkins.setSecurityRealm(securityRealm); + WebClient wc = j.createWebClient(); + + spyApiTokenListener.usersWithDeletedTokens.clear(); + assertTrue(spyApiTokenListener.usersWithDeletedTokens.isEmpty()); + + // new user account creation + SignupPage signup = new SignupPage(wc.goTo("signup")); + signup.enterUsername("alice"); + signup.enterPassword("alice"); + signup.enterFullName(StringUtils.capitalize("alice user")); + signup.enterEmail("alice" + "@" + "example.com"); + HtmlPage page = signup.submit(j); + + // execute an http request to create a new a user api token from their config page + User alice = User.getById("alice", false); + URL configPage = wc.createCrumbedUrl(alice.getUrl() + "/" + "/descriptorByName/" + ApiTokenProperty.class.getName() + "/generateNewToken/?newTokenName=" + "alice-token"); + Page p = wc.getPage(new WebRequest(configPage, HttpMethod.POST)); + JSONObject responseJson = JSONObject.fromObject(p.getWebResponse().getContentAsString()); + GenerateNewTokenResponse userToken = (GenerateNewTokenResponse) responseJson.getJSONObject("data").toBean(GenerateNewTokenResponse.class); + assertNotNull(userToken.tokenUuid); + + // execute a second http request to delete the just created user api token from their config page + configPage = wc.createCrumbedUrl(alice.getUrl() + "/" + "/descriptorByName/" + ApiTokenProperty.class.getName() + "/revoke/?tokenUuid=" + userToken.tokenUuid); + p = wc.getPage(new WebRequest(configPage, HttpMethod.POST)); + + // ensure user whose new token was deleted was in fact logged + assertEquals(200, p.getWebResponse().getStatusCode()); + assertEquals("alice", spyApiTokenListener.usersWithDeletedTokens.get(0)); + } + @Test public void postWithUsernameAndTokenInBasicAuthHeader() throws Exception { FreeStyleProject p = j.createFreeStyleProject("bar"); @@ -460,7 +533,26 @@ private GenerateNewTokenResponse generateNewToken(WebClient wc, String login, St Object result = responseJson.getJSONObject("data").toBean(GenerateNewTokenResponse.class); return (GenerateNewTokenResponse) result; } - - + + @TestExtension + public static class SpyApiTokenPropertyListenerImpl extends ApiTokenPropertyListener { + private List usersWithCreatedTokens = new ArrayList<>(); + private List usersWithDeletedTokens = new ArrayList<>(); + + @Override + public void onCreated(@Nonnull String username, @Nonnull UserProperty value) { + if (value instanceof ApiTokenProperty) { + usersWithCreatedTokens.add(username); + } + } + + @Override + public void onDeleted(@Nonnull String username, @Nonnull UserProperty value) { + if (value instanceof ApiTokenProperty) { + usersWithDeletedTokens.add(username); + } + } + } + // test no token are generated for new user with the global configuration set to false }