Repository: gravitee-io/graviteeio-access-management
Pull request #663 (2019-10-15 12:56:40)

fix(users): unlock user who has never been authenticated
fixes gravitee-io/issues#2736


Patch:
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java
index 8772bc9046..76c64aaf08 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/UserService.java
@@ -51,6 +51,8 @@ public interface UserService {
 
     Single<User> create(String domain, NewUser newUser);
 
+    Single<User> create(User user);
+
     Single<User> update(String domain, String id, UpdateUser updateUser);
 
     Single<User> update(User user);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/LoginAttemptServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/LoginAttemptServiceImpl.java
index 77c20f78b9..9efcc912ba 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/LoginAttemptServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/LoginAttemptServiceImpl.java
@@ -18,6 +18,7 @@ package io.gravitee.am.service.impl;
 import io.gravitee.am.common.audit.EventType;
 import io.gravitee.am.common.utils.RandomString;
 import io.gravitee.am.model.LoginAttempt;
+import io.gravitee.am.model.User;
 import io.gravitee.am.model.account.AccountSettings;
 import io.gravitee.am.repository.management.api.LoginAttemptRepository;
 import io.gravitee.am.repository.management.api.search.LoginAttemptCriteria;
@@ -32,6 +33,7 @@ import io.gravitee.am.service.reporter.builder.AuditBuilder;
 import io.gravitee.am.service.reporter.builder.management.UserAuditBuilder;
 import io.reactivex.Completable;
 import io.reactivex.Maybe;
+import io.reactivex.Single;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -99,9 +101,6 @@ public class LoginAttemptServiceImpl implements LoginAttemptService {
                     return Completable.complete();
                 })
                 .onErrorResumeNext(ex -> {
-                    if (ex instanceof UserNotFoundException) {
-                        return Completable.complete();
-                    }
                     if (ex instanceof AbstractManagementException) {
                         return Completable.error(ex);
                     }
@@ -159,6 +158,21 @@ public class LoginAttemptServiceImpl implements LoginAttemptService {
                     user.setAccountLockedUntil(new Date(System.currentTimeMillis() + (accountSettings.getAccountBlockedDuration() * 1000)));
                     return userService.update(user);
                 })
+                .onErrorResumeNext(ex -> {
+                    if (ex instanceof UserNotFoundException) {
+                        final User newUser = new User();
+                        newUser.setUsername(criteria.username());
+                        newUser.setDomain(criteria.domain());
+                        newUser.setClient(criteria.client());
+                        newUser.setSource(criteria.identityProvider());
+                        newUser.setLoginsCount(0l);
+                        newUser.setAccountNonLocked(false);
+                        newUser.setAccountLockedAt(new Date());
+                        newUser.setAccountLockedUntil(new Date(System.currentTimeMillis() + (accountSettings.getAccountBlockedDuration() * 1000)));
+                        return userService.create(newUser);
+                    }
+                    return Single.error(ex);
+                })
                 .doOnSuccess(user -> auditService.report(AuditBuilder.builder(UserAuditBuilder.class).type(EventType.USER_LOCKED).domain(criteria.domain()).client(criteria.client()).principal(null).user(user)))
                 .toCompletable();
     }
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
index 1c6b9c186a..7dc2f0730d 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/UserServiceImpl.java
@@ -190,6 +190,21 @@ public class UserServiceImpl implements UserService {
                 });
     }
 
+    @Override
+    public Single<User> create(User user) {
+        LOGGER.debug("Create a user {}", user);
+        user.setCreatedAt(new Date());
+        user.setUpdatedAt(user.getCreatedAt());
+        return userRepository.create(user)
+                .onErrorResumeNext(ex -> {
+                    if (ex instanceof AbstractManagementException) {
+                        return Single.error(ex);
+                    }
+                    LOGGER.error("An error occurs while trying to create a user", ex);
+                    return Single.error(new TechnicalManagementException("An error occurs while trying to create a user", ex));
+                });
+    }
+
     @Override
     public Single<User> update(String domain, String id, UpdateUser updateUser) {
         LOGGER.debug("Update a user {} for domain {}", id, domain);
diff --git a/gravitee-am-ui/src/app/domain/clients/client/account/account.component.ts b/gravitee-am-ui/src/app/domain/clients/client/account/account.component.ts
index 4f324599e6..67ee942aaf 100644
--- a/gravitee-am-ui/src/app/domain/clients/client/account/account.component.ts
+++ b/gravitee-am-ui/src/app/domain/clients/client/account/account.component.ts
@@ -36,7 +36,7 @@ export class ClientAccountSettingsComponent {
   ngOnInit() {
     this.domainId = this.route.snapshot.parent.parent.params['domainId'];
     this.client = this.route.snapshot.parent.data['client'];
-    this.accountSettings = Object.assign({}, this.client.accountSettings);
+    this.accountSettings = this.client.accountSettings || { 'inherited' : true };
   }
 
   updateAccountSettings(accountSettings) {
diff --git a/gravitee-am-ui/src/app/domain/components/account/account-settings.component.ts b/gravitee-am-ui/src/app/domain/components/account/account-settings.component.ts
index 8f29a81679..3a3cb8bbf4 100644
--- a/gravitee-am-ui/src/app/domain/components/account/account-settings.component.ts
+++ b/gravitee-am-ui/src/app/domain/components/account/account-settings.component.ts
@@ -53,7 +53,6 @@ export class AccountSettingsComponent implements OnInit, OnChanges {
   }
 
   ngOnInit(): void {
-    this.accountSettings = this.accountSettings || { 'inherited' : this.inheritMode };
     this.initDateValues();
   }
 


Test patch:
diff --git a/gravitee-am-service/src/test/java/io/gravitee/am/service/LoginAttemptServiceTest.java b/gravitee-am-service/src/test/java/io/gravitee/am/service/LoginAttemptServiceTest.java
new file mode 100644
index 0000000000..b1e5e42a7b
--- /dev/null
+++ b/gravitee-am-service/src/test/java/io/gravitee/am/service/LoginAttemptServiceTest.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
+ *
+ * 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 io.gravitee.am.service;
+
+import io.gravitee.am.model.LoginAttempt;
+import io.gravitee.am.model.User;
+import io.gravitee.am.model.account.AccountSettings;
+import io.gravitee.am.repository.management.api.LoginAttemptRepository;
+import io.gravitee.am.repository.management.api.search.LoginAttemptCriteria;
+import io.gravitee.am.service.impl.LoginAttemptServiceImpl;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
+ * @author GraviteeSource Team
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class LoginAttemptServiceTest {
+
+    @InjectMocks
+    private LoginAttemptService loginAttemptService = new LoginAttemptServiceImpl();
+
+    @Mock
+    private LoginAttemptRepository loginAttemptRepository;
+
+    @Mock
+    private UserService userService;
+
+    @Mock
+    private AuditService auditService;
+
+    @Test
+    public void shouldCreateUser_accountLockFirstConnection() {
+        final LoginAttemptCriteria loginAttemptCriteria = new LoginAttemptCriteria.Builder()
+                .client("client-1")
+                .domain("domain-1")
+                .username("user-1")
+                .identityProvider("idp-1")
+                .build();
+
+        final LoginAttempt loginAttempt = new LoginAttempt();
+        loginAttempt.setAttempts(1);
+
+        final AccountSettings accountSettings = new AccountSettings();
+        accountSettings.setLoginAttemptsDetectionEnabled(true);
+        accountSettings.setMaxLoginAttempts(1);
+        accountSettings.setAccountBlockedDuration(24 * 60 * 60 * 1000);
+
+        when(loginAttemptRepository.findByCriteria(loginAttemptCriteria)).thenReturn(Maybe.just(loginAttempt));
+        when(loginAttemptRepository.update(loginAttempt)).thenReturn(Single.just(loginAttempt));
+        when(userService.findByDomainAndUsernameAndSource(anyString(), anyString(), anyString())).thenReturn(Maybe.empty());
+        when(userService.create(any())).thenReturn(Single.just(new User()));
+
+        TestObserver testObserver = loginAttemptService.loginFailed(loginAttemptCriteria, accountSettings).test();
+        testObserver.awaitTerminalEvent();
+        testObserver.assertNoErrors();
+
+        verify(userService, times(1)).create(any());
+    }
+
+    @Test
+    public void shouldUpdateUser_accountLockAlreadyRegistered() {
+        final LoginAttemptCriteria loginAttemptCriteria = new LoginAttemptCriteria.Builder()
+                .client("client-1")
+                .domain("domain-1")
+                .username("user-1")
+                .identityProvider("idp-1")
+                .build();
+
+        final LoginAttempt loginAttempt = new LoginAttempt();
+        loginAttempt.setAttempts(1);
+
+        final AccountSettings accountSettings = new AccountSettings();
+        accountSettings.setLoginAttemptsDetectionEnabled(true);
+        accountSettings.setMaxLoginAttempts(1);
+        accountSettings.setAccountBlockedDuration(24 * 60 * 60 * 1000);
+
+        when(loginAttemptRepository.findByCriteria(loginAttemptCriteria)).thenReturn(Maybe.just(loginAttempt));
+        when(loginAttemptRepository.update(loginAttempt)).thenReturn(Single.just(loginAttempt));
+        when(userService.findByDomainAndUsernameAndSource(anyString(), anyString(), anyString())).thenReturn(Maybe.just(new User()));
+        when(userService.update(any())).thenReturn(Single.just(new User()));
+
+        TestObserver testObserver = loginAttemptService.loginFailed(loginAttemptCriteria, accountSettings).test();
+        testObserver.awaitTerminalEvent();
+        testObserver.assertNoErrors();
+
+        verify(userService, never()).create(any());
+        verify(userService, times(1)).update(any());
+    }
+}