Repository: gravitee-io/graviteeio-access-management
Pull request #966 (2020-11-17 21:25:55)

fix(users): delete administrative users (organization)
fixes gravitee-io/issues#4622


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000000..558804d71f
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,30 @@
+version: 2.1
+
+parameters:
+  gio_action:
+    type: enum
+    enum: [release, pr_build]
+    default: pr_build
+  dry_run:
+    type: boolean
+    default: true
+    description: "Run in dry run mode?"
+
+orbs:
+  gravitee: gravitee-io/gravitee@1.0
+
+workflows:
+  version: 2.1
+  pull_requests:
+    when:
+      equal: [ pr_build, << pipeline.parameters.gio_action >> ]
+    jobs:
+      - gravitee/pr-build:
+          context: cicd-orchestrator
+  release:
+    when:
+      equal: [ release, << pipeline.parameters.gio_action >> ]
+    jobs:
+      - gravitee/release:
+          context: cicd-orchestrator
+          dry_run: << pipeline.parameters.dry_run >>
diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/handler/login/LoginCallbackFailureHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/handler/login/LoginCallbackFailureHandler.java
index 4e1faf9666..f25617b71e 100644
--- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/handler/login/LoginCallbackFailureHandler.java
+++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/handler/login/LoginCallbackFailureHandler.java
@@ -79,8 +79,12 @@ public class LoginCallbackFailureHandler implements Handler<RoutingContext> {
             String uri = UriBuilderRequest.resolveProxyRequest(context.request(), context.request().path().replaceFirst("/callback", ""), params);
             doRedirect(context.response(), uri);
         } catch (Exception ex) {
-            logger.error("An error occurs while redirecting to the login page", ex);
-            context.fail(503);
+            logger.error("An error has occurred while redirecting to the login page", ex);
+            // Note: we can't invoke context.fail cause it'll lead to infinite failure handling.
+            context
+                    .response()
+                    .setStatusCode(HttpStatusCode.SERVICE_UNAVAILABLE_503)
+                    .end();
         }
     }
 
diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java
index f0706ad3fa..d9236ce838 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/main/java/io/gravitee/am/management/service/impl/UserServiceImpl.java
@@ -30,6 +30,7 @@ import io.gravitee.am.model.*;
 import io.gravitee.am.model.account.AccountSettings;
 import io.gravitee.am.model.common.Page;
 import io.gravitee.am.model.factor.EnrolledFactor;
+import io.gravitee.am.model.membership.MemberType;
 import io.gravitee.am.repository.management.api.search.LoginAttemptCriteria;
 import io.gravitee.am.service.*;
 import io.gravitee.am.service.exception.*;
@@ -97,6 +98,9 @@ public class UserServiceImpl implements UserService {
     @Autowired
     private DomainService domainService;
 
+    @Autowired
+    private MembershipService membershipService;
+
     @Override
     public Single<Page<User>> search(ReferenceType referenceType, String referenceId, String query, int page, int size) {
         return userService.search(referenceType, referenceId, query, page, size);
@@ -330,6 +334,13 @@ public class UserServiceImpl implements UserService {
                                     });
                         })
                         .andThen(userService.delete(userId))
+                        // remove from memberships if user is an administrative user
+                        .andThen((ReferenceType.ORGANIZATION != referenceType) ? Completable.complete() :
+                                membershipService.findByMember(userId, MemberType.USER)
+                                    .flatMapCompletable(memberships -> {
+                                        List<Completable> deleteMembershipsCompletable = memberships.stream().map(m -> membershipService.delete(m.getId())).collect(Collectors.toList());
+                                        return Completable.concat(deleteMembershipsCompletable);
+                                    }))
                         .doOnComplete(() -> auditService.report(AuditBuilder.builder(UserAuditBuilder.class).principal(principal).type(EventType.USER_DELETED).user(user)))
                         .doOnError(throwable -> auditService.report(AuditBuilder.builder(UserAuditBuilder.class).principal(principal).type(EventType.USER_DELETED).throwable(throwable)))
                 );
diff --git a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/MembershipRepository.java b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/MembershipRepository.java
index c42c7a2e7e..ac6fde25df 100644
--- a/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/MembershipRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-api/src/main/java/io/gravitee/am/repository/management/api/MembershipRepository.java
@@ -34,6 +34,8 @@ public interface MembershipRepository extends CrudRepository<Membership, String>
 
     Single<List<Membership>> findByReference(String referenceId, ReferenceType referenceType);
 
+    Single<List<Membership>> findByMember(String memberId, MemberType memberType);
+
     Flowable<Membership> findByCriteria(ReferenceType referenceType, String referenceId, MembershipCriteria criteria);
 
     Maybe<Membership> findByReferenceAndMember(ReferenceType referenceType, String referenceId, MemberType memberType, String memberId);
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoMembershipRepository.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoMembershipRepository.java
index 752fb258fd..2d1df18da2 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoMembershipRepository.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/main/java/io/gravitee/am/repository/mongodb/management/MongoMembershipRepository.java
@@ -55,6 +55,7 @@ public class MongoMembershipRepository extends AbstractManagementMongoRepository
         membershipsCollection = mongoOperations.getCollection("memberships", MembershipMongo.class);
         super.createIndex(membershipsCollection, new Document(FIELD_REFERENCE_ID, 1).append(FIELD_REFERENCE_TYPE, 1));
         super.createIndex(membershipsCollection, new Document(FIELD_REFERENCE_ID, 1).append(FIELD_MEMBER_ID, 1));
+        super.createIndex(membershipsCollection, new Document(FIELD_MEMBER_ID, 1).append(FIELD_MEMBER_TYPE, 1));
     }
 
     @Override
@@ -63,6 +64,12 @@ public class MongoMembershipRepository extends AbstractManagementMongoRepository
                 .map(this::convert).collect(ArrayList::new, List::add);
     }
 
+    @Override
+    public Single<List<Membership>> findByMember(String memberId, MemberType memberType) {
+        return Observable.fromPublisher(membershipsCollection.find(and(eq(FIELD_MEMBER_ID, memberId), eq(FIELD_MEMBER_TYPE, memberType.name()))))
+                .map(this::convert).collect(ArrayList::new, List::add);
+    }
+
     @Override
     public Flowable<Membership> findByCriteria(ReferenceType referenceType, String referenceId, MembershipCriteria criteria) {
 
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/MembershipService.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/MembershipService.java
index d4ce4ee247..c6485949f8 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/MembershipService.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/MembershipService.java
@@ -18,6 +18,7 @@ package io.gravitee.am.service;
 import io.gravitee.am.identityprovider.api.User;
 import io.gravitee.am.model.Membership;
 import io.gravitee.am.model.ReferenceType;
+import io.gravitee.am.model.membership.MemberType;
 import io.gravitee.am.repository.management.api.search.MembershipCriteria;
 import io.reactivex.Completable;
 import io.reactivex.Flowable;
@@ -39,6 +40,8 @@ public interface MembershipService {
 
     Single<List<Membership>> findByReference(String referenceId, ReferenceType referenceType);
 
+    Single<List<Membership>> findByMember(String memberId, MemberType memberType);
+
     Single<Membership> addOrUpdate(String organizationId, Membership membership, User principal);
 
     Single<Map<String, Map<String, Object>>> getMetadata(List<Membership> memberships);
diff --git a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/MembershipServiceImpl.java b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/MembershipServiceImpl.java
index 0535d61f6c..1b6d1d227c 100644
--- a/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/MembershipServiceImpl.java
+++ b/gravitee-am-service/src/main/java/io/gravitee/am/service/impl/MembershipServiceImpl.java
@@ -28,7 +28,6 @@ import io.gravitee.am.model.common.event.Event;
 import io.gravitee.am.model.common.event.Payload;
 import io.gravitee.am.model.membership.Member;
 import io.gravitee.am.model.membership.MemberType;
-import io.gravitee.am.model.permissions.SystemRole;
 import io.gravitee.am.repository.management.api.MembershipRepository;
 import io.gravitee.am.repository.management.api.search.MembershipCriteria;
 import io.gravitee.am.service.*;
@@ -40,7 +39,6 @@ import io.reactivex.Completable;
 import io.reactivex.Flowable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
-import io.reactivex.internal.operators.maybe.MaybeError;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -75,12 +73,6 @@ public class MembershipServiceImpl implements MembershipService {
     @Autowired
     private RoleService roleService;
 
-    @Autowired
-    private DomainService domainService;
-
-    @Autowired
-    private OrganizationService organizationService;
-
     @Autowired
     private EventService eventService;
 
@@ -113,6 +105,17 @@ public class MembershipServiceImpl implements MembershipService {
                 });
     }
 
+    @Override
+    public Single<List<Membership>> findByMember(String memberId, MemberType memberType) {
+        LOGGER.debug("Find memberships by member id {} and member type {}", memberId, memberType);
+        return membershipRepository.findByMember(memberId, memberType)
+                .onErrorResumeNext(ex -> {
+                    LOGGER.error("An error occurs while trying to find memberships by member id {} and member type {}", memberId, memberType, ex);
+                    return Single.error(new TechnicalManagementException(
+                            String.format("An error occurs while trying to find memberships by member id %s and member type %s", memberId, memberType), ex));
+                });
+    }
+
     @Override
     public Single<Membership> addOrUpdate(String organizationId, Membership membership, User principal) {
         LOGGER.debug("Add or update membership {}", membership);
diff --git a/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts
index 2746d16a55..a6325db412 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/users/user/profile/profile.component.ts
@@ -73,15 +73,18 @@ export class UserProfileComponent implements OnInit {
   }
 
   delete(event) {
-    // TODO we should be able to delete platform users
     event.preventDefault();
     this.dialogService
       .confirm('Delete User', 'Are you sure you want to delete this user ?')
       .subscribe(res => {
         if (res) {
-          this.userService.delete(this.domainId, this.user.id).subscribe(response => {
+          this.userService.delete(this.domainId, this.user.id, this.organizationContext).subscribe(response => {
             this.snackbarService.open('User ' + this.user.username + ' deleted');
-            this.router.navigate(['/domains', this.domainId, 'settings', 'users']);
+            if (this.organizationContext) {
+              this.router.navigate(['/settings', 'management', 'users']);
+            } else {
+              this.router.navigate(['/domains', this.domainId, 'settings', 'users']);
+            }
           });
         }
       });
diff --git a/gravitee-am-ui/src/app/domain/settings/users/users.component.ts b/gravitee-am-ui/src/app/domain/settings/users/users.component.ts
index 9898cc01cc..19de20be47 100644
--- a/gravitee-am-ui/src/app/domain/settings/users/users.component.ts
+++ b/gravitee-am-ui/src/app/domain/settings/users/users.component.ts
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 import { Component, OnInit } from '@angular/core';
-import { UserService} from "../../../services/user.service";
-import { SnackbarService } from "../../../services/snackbar.service";
-import { DialogService } from "../../../services/dialog.service";
-import { ActivatedRoute, Router } from "@angular/router";
-import { OrganizationService } from "../../../services/organization.service";
-import {AuthService} from "../../../services/auth.service";
+import { ActivatedRoute, Router } from '@angular/router';
+import { UserService} from '../../../services/user.service';
+import { SnackbarService } from '../../../services/snackbar.service';
+import { DialogService } from '../../../services/dialog.service';
+import { OrganizationService } from '../../../services/organization.service';
+import {AuthService} from '../../../services/auth.service';
 
 @Component({
   selector: 'app-users',
@@ -67,7 +67,7 @@ export class UsersComponent implements OnInit {
   }
 
   loadUsers() {
-    let findUsers = (this.searchValue) ?
+    const findUsers = (this.searchValue) ?
       this.userService.search(this.domainId, this.searchValue + '*', this.page.pageNumber, this.page.size, this.organizationContext) :
       (this.organizationContext ? this.organizationService.users(this.page.pageNumber, this.page.size) : this.userService.findByDomain(this.domainId, this.page.pageNumber, this.page.size));
 
@@ -83,8 +83,8 @@ export class UsersComponent implements OnInit {
       .confirm('Delete User', 'Are you sure you want to delete this user ?')
       .subscribe(res => {
         if (res) {
-          this.userService.delete(this.domainId, id).subscribe(response => {
-            this.snackbarService.open("User deleted");
+          this.userService.delete(this.domainId, id, this.organizationContext).subscribe(response => {
+            this.snackbarService.open('User deleted');
             this.page.pageNumber = 0;
             this.loadUsers();
           });
diff --git a/gravitee-am-ui/src/app/services/organization.service.ts b/gravitee-am-ui/src/app/services/organization.service.ts
index bf1945821b..67f4516ffe 100644
--- a/gravitee-am-ui/src/app/services/organization.service.ts
+++ b/gravitee-am-ui/src/app/services/organization.service.ts
@@ -17,7 +17,7 @@ import { Injectable } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 import { AppConfig } from '../../config/app.config';
-import {map} from "rxjs/operators";
+import {map} from 'rxjs/operators';
 
 @Injectable()
 export class OrganizationService {
@@ -237,6 +237,10 @@ export class OrganizationService {
     return this.http.get<any>(this.organizationURL + '/users/' + userId);
   }
 
+  deleteUser(userId): Observable<any> {
+    return this.http.delete<any>(this.organizationURL + '/users/' + userId);
+  }
+
   userRoles(userId): Observable<any>  {
     return this.http.get<any>(this.organizationURL + '/users/' + userId + '/roles');
   }
diff --git a/gravitee-am-ui/src/app/services/user.service.ts b/gravitee-am-ui/src/app/services/user.service.ts
index 7f773d0ed7..f6b448d83f 100644
--- a/gravitee-am-ui/src/app/services/user.service.ts
+++ b/gravitee-am-ui/src/app/services/user.service.ts
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 import {Injectable} from '@angular/core';
-import {AppConfig} from "../../config/app.config";
-import {HttpClient} from "@angular/common/http";
-import {Observable} from "rxjs";
-import {OrganizationService} from "./organization.service";
+import {AppConfig} from '../../config/app.config';
+import {HttpClient} from '@angular/common/http';
+import {Observable} from 'rxjs';
+import {OrganizationService} from './organization.service';
 
 @Injectable()
 export class UserService {
@@ -27,19 +27,19 @@ export class UserService {
               private organizationService: OrganizationService) { }
 
   findByDomain(domainId, page, size): Observable<any> {
-    return this.http.get<any>(this.usersURL + domainId + "/users?page=" + page + "&size=" + size);
+    return this.http.get<any>(this.usersURL + domainId + '/users?page=' + page + '&size=' + size);
   }
 
   get(domainId, id): Observable<any> {
-    return this.http.get<any>(this.usersURL + domainId + "/users/" + id);
+    return this.http.get<any>(this.usersURL + domainId + '/users/' + id);
   }
 
   create(domainId, user): Observable<any> {
-    return this.http.post<any>(this.usersURL + domainId + "/users", user);
+    return this.http.post<any>(this.usersURL + domainId + '/users', user);
   }
 
   update(domainId, id, user): Observable<any> {
-    return this.http.put<any>(this.usersURL + domainId + "/users/" + id, {
+    return this.http.put<any>(this.usersURL + domainId + '/users/' + id, {
       'firstName' : user.firstName,
       'lastName' : user.lastName,
       'email' : user.email,
@@ -50,64 +50,67 @@ export class UserService {
   }
 
   updateStatus(domainId, id, status): Observable<any> {
-    return this.http.put<any>(this.usersURL + domainId + "/users/" + id + '/status', {
+    return this.http.put<any>(this.usersURL + domainId + '/users/' + id + '/status', {
       'enabled' : status
     });
   }
 
-  delete(domainId, id): Observable<any> {
-    return this.http.delete<any>(this.usersURL + domainId + "/users/" + id);
+  delete(domainId, id, organizationContext): Observable<any> {
+    if (organizationContext) {
+      return this.organizationService.deleteUser(id);
+    }
+    return this.http.delete<any>(this.usersURL + domainId + '/users/' + id);
   }
 
   resendRegistrationConfirmation(domainId, id): Observable<any> {
-    return this.http.post<any>(this.usersURL + domainId + "/users/" + id + "/sendRegistrationConfirmation", {});
+    return this.http.post<any>(this.usersURL + domainId + '/users/' + id + '/sendRegistrationConfirmation', {});
   }
 
   resetPassword(domainId, id, password): Observable<any> {
-    return this.http.post<any>(this.usersURL + domainId + "/users/" + id + "/resetPassword", {
+    return this.http.post<any>(this.usersURL + domainId + '/users/' + id + '/resetPassword', {
       'password': password
     });
   }
 
   unlock(domainId, id): Observable<any> {
-    return this.http.post<any>(this.usersURL + domainId + "/users/" + id + "/unlock", {});
+    return this.http.post<any>(this.usersURL + domainId + '/users/' + id + '/unlock', {});
   }
 
   search(domainId, searchTerm, page, size, organizationContext): Observable<any> {
     if (organizationContext) {
       return this.organizationService.searchUsers(searchTerm, page, size);
     }
-    return this.http.get<any>(this.usersURL + domainId + "/users?q=" + searchTerm + "&page=" + page + "&size=" + size);
+    return this.http.get<any>(this.usersURL + domainId + '/users?q=' + searchTerm + '&page=' + page + '&size=' + size);
   }
 
   consents(domainId, userId, clientId): Observable<any> {
-    return this.http.get<any>(this.usersURL + domainId + "/users/" + userId + "/consents" + (clientId ? "?clientId=" + clientId : ""));
+    return this.http.get<any>(this.usersURL + domainId + '/users/' + userId + '/consents' + (clientId ? '?clientId=' + clientId : ''));
   }
 
   revokeConsents(domainId, userId, clientId): Observable<any> {
-    return this.http.delete<any>(this.usersURL + domainId + "/users/" + userId + "/consents" + (clientId ? "?clientId=" + clientId : ""));
+    return this.http.delete<any>(this.usersURL + domainId + '/users/' + userId + '/consents' + (clientId ? '?clientId=' + clientId : ''));
   }
 
   revokeConsent(domainId, userId, consentId): Observable<any> {
-    return this.http.delete<any>(this.usersURL + domainId + "/users/" + userId + "/consents/" + consentId);
+    return this.http.delete<any>(this.usersURL + domainId + '/users/' + userId + '/consents/' + consentId);
   }
 
   roles(domainId, userId): Observable<any> {
-    return this.http.get<any>(this.usersURL + domainId + "/users/" + userId + "/roles");
+    return this.http.get<any>(this.usersURL + domainId + '/users/' + userId + '/roles');
   }
 
   revokeRole(domainId, userId, roleId, organizationContext): Observable<any> {
     if (organizationContext) {
       return this.organizationService.revokeUserRole(userId, roleId);
     }
-    return this.http.delete<any>(this.usersURL + domainId + "/users/" + userId + "/roles/" + roleId);
+    return this.http.delete<any>(this.usersURL + domainId + '/users/' + userId + '/roles/' + roleId);
   }
 
   assignRoles(domainId, userId, roles, organizationContext): Observable<any> {
     if (organizationContext) {
       return this.organizationService.assignUserRoles(userId, roles);
     }
-    return this.http.post<any>(this.usersURL + domainId + "/users/" + userId + "/roles", roles);
+    return this.http.post<any>(this.usersURL + domainId + '/users/' + userId + '/roles', roles);
   }
 
   factors(domainId, userId): Observable<any> {


Test patch:
diff --git a/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/UserServiceTest.java b/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/UserServiceTest.java
index 2bfa75e625..dc97e0e7ec 100644
--- a/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/UserServiceTest.java
+++ b/gravitee-am-management-api/gravitee-am-management-api-service/src/test/java/io/gravitee/am/management/service/UserServiceTest.java
@@ -18,11 +18,7 @@ package io.gravitee.am.management.service;
 import io.gravitee.am.identityprovider.api.DefaultUser;
 import io.gravitee.am.identityprovider.api.UserProvider;
 import io.gravitee.am.management.service.impl.UserServiceImpl;
-import io.gravitee.am.model.Application;
-import io.gravitee.am.model.ReferenceType;
-import io.gravitee.am.model.Domain;
-import io.gravitee.am.model.Role;
-import io.gravitee.am.model.User;
+import io.gravitee.am.model.*;
 import io.gravitee.am.model.application.ApplicationSettings;
 import io.gravitee.am.service.ApplicationService;
 import io.gravitee.am.model.account.AccountSettings;
@@ -38,6 +34,7 @@ import io.gravitee.am.service.model.NewUser;
 import io.gravitee.am.service.model.UpdateUser;
 import io.jsonwebtoken.JwtBuilder;
 import io.reactivex.Completable;
+import io.reactivex.Flowable;
 import io.reactivex.Maybe;
 import io.reactivex.Single;
 import io.reactivex.observers.TestObserver;
@@ -91,6 +88,9 @@ public class UserServiceTest {
     @Mock
     private JwtBuilder jwtBuilder;
 
+    @Mock
+    private MembershipService membershipService;
+
     @Before
     public void setUp() {
         ((UserServiceImpl) userService).setExpireAfter(24 * 3600);
@@ -475,4 +475,51 @@ public class UserServiceTest {
         }
     }
 
+    @Test
+    public void shouldDeleteUser_without_membership() {
+        final String organization = "DEFAULT";
+        final String userId = "user-id";
+        User user = mock(User.class);
+        when(user.getId()).thenReturn(userId);
+        when(user.getSource()).thenReturn("source-idp");
+        when(commonUserService.findById(any(), any(), any())).thenReturn(Single.just(user));
+        when(identityProviderManager.getUserProvider(any())).thenReturn(Maybe.empty());
+        when(commonUserService.delete(anyString())).thenReturn(Completable.complete());
+        when(membershipService.findByMember(any(), any())).thenReturn(Single.just(Collections.emptyList()));
+
+        TestObserver<Void> testObserver = userService.delete(ReferenceType.ORGANIZATION, organization, userId).test();
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(commonUserService, times(1)).delete(any());
+        verify(membershipService, never()).delete(anyString());
+    }
+
+    @Test
+    public void shouldDeleteUser_with_memberships() {
+        final String organization = "DEFAULT";
+        final String userId = "user-id";
+
+        User user = mock(User.class);
+        when(user.getId()).thenReturn(userId);
+        when(user.getSource()).thenReturn("source-idp");
+
+        Membership m1 = mock(Membership.class);
+        when(m1.getId()).thenReturn("m1");
+        Membership m2 = mock(Membership.class);
+        when(m2.getId()).thenReturn("m2");
+        Membership m3 = mock(Membership.class);
+        when(m3.getId()).thenReturn("m3");
+
+        when(commonUserService.findById(any(), any(), any())).thenReturn(Single.just(user));
+        when(identityProviderManager.getUserProvider(any())).thenReturn(Maybe.empty());
+        when(commonUserService.delete(anyString())).thenReturn(Completable.complete());
+        when(membershipService.findByMember(any(), any())).thenReturn(Single.just(Arrays.asList(m1, m2, m3)));
+        when(membershipService.delete(anyString())).thenReturn(Completable.complete());
+
+        TestObserver<Void> testObserver = userService.delete(ReferenceType.ORGANIZATION, organization, userId).test();
+        testObserver.assertComplete();
+        testObserver.assertNoErrors();
+        verify(commonUserService, times(1)).delete(any());
+        verify(membershipService, times(3)).delete(anyString());
+    }
 }
diff --git a/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoMembershipRepositoryTest.java b/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoMembershipRepositoryTest.java
index 27fd4f2100..8450179e1f 100644
--- a/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoMembershipRepositoryTest.java
+++ b/gravitee-am-repository/gravitee-am-repository-mongodb/src/test/java/io/gravitee/am/repository/mongodb/management/MongoMembershipRepositoryTest.java
@@ -15,11 +15,9 @@
  */
 package io.gravitee.am.repository.mongodb.management;
 
-import io.gravitee.am.model.IdentityProvider;
 import io.gravitee.am.model.Membership;
 import io.gravitee.am.model.ReferenceType;
 import io.gravitee.am.model.membership.MemberType;
-import io.gravitee.am.repository.exceptions.TechnicalException;
 import io.gravitee.am.repository.management.api.MembershipRepository;
 import io.gravitee.am.repository.management.api.search.MembershipCriteria;
 import io.reactivex.observers.TestObserver;
@@ -89,6 +87,34 @@ public class MongoMembershipRepositoryTest extends AbstractManagementRepositoryT
         obs.assertValue(m -> m.size() == 1 && m.get(0).getId().equals(createdMembership.getId()));
     }
 
+    @Test
+    public void testFindByMember() {
+
+        Membership membership = new Membership();
+        membership.setRoleId("role#1");
+        membership.setReferenceType(ReferenceType.ORGANIZATION);
+        membership.setReferenceId(ORGANIZATION_ID);
+        membership.setMemberType(MemberType.USER);
+        membership.setMemberId("user#1");
+
+        Membership membership2 = new Membership();
+        membership2.setRoleId("role#1");
+        membership2.setReferenceType(ReferenceType.DOMAIN);
+        membership2.setReferenceId("DOMAIN_ID");
+        membership2.setMemberType(MemberType.USER);
+        membership2.setMemberId("user#1");
+
+        membershipRepository.create(membership).blockingGet();
+        membershipRepository.create(membership2).blockingGet();
+
+        TestObserver<List<Membership>> obs = membershipRepository.findByMember("user#1", MemberType.USER).test();
+        obs.awaitTerminalEvent();
+
+        obs.assertComplete();
+        obs.assertValue(m -> m.size() == 2);
+    }
+
+
     @Test
     public void testFindByReferenceAndMember() {
 
@@ -166,4 +192,4 @@ public class MongoMembershipRepositoryTest extends AbstractManagementRepositoryT
         obs.assertValueCount(2);
     }
 
-}
\ No newline at end of file
+}