From f63b310a70a87370e6bf822a6728b711fad0c0c3 Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Mon, 13 Apr 2020 14:38:05 +0200
Subject: [PATCH] Removed Spring dependency to gms-client-lib (used built-in
 java.net.HttpClient); used access token instead of BasicAuth; Added missing
 endpoints in GMS JWT web service controller

---
 gms-client/gms-client-lib/pom.xml             |  15 +-
 .../it/inaf/ia2/gms/client/GmsClient.java     | 133 ++++---------
 .../ia2/gms/client/call/AddMemberCall.java    |  34 ++++
 .../gms/client/call/AddPermissionCall.java    |  39 ++++
 .../inaf/ia2/gms/client/call/BaseGmsCall.java |  45 +++++
 .../ia2/gms/client/call/CreateGroupCall.java  |  29 +++
 .../ia2/gms/client/call/DeleteGroupCall.java  |  28 +++
 .../gms/client/call/GetUserGroupsCall.java    |  91 +++++++++
 .../gms/client/call/HttpClientWrapper.java    |  41 ++++
 .../ia2/gms/client/call/ListGroupsCall.java   |  54 +++++
 .../ia2/gms/client/call/RemoveMemberCall.java |  34 ++++
 .../gms/client/call/RemovePermissionCall.java |  35 ++++
 .../it/inaf/ia2/gms/client/model/Group.java   |  41 ----
 .../it/inaf/ia2/gms/client/model/Member.java  |  23 ---
 .../inaf/ia2/gms/client/model/Permission.java |  33 +---
 .../it/inaf/ia2/gms/client/GmsClientTest.java | 187 ++++++++++--------
 .../client/call/MockedHttpClientWrapper.java  |  18 ++
 .../controller/JWTWebServiceController.java   |  28 +++
 18 files changed, 621 insertions(+), 287 deletions(-)
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Group.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Member.java
 create mode 100644 gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java

diff --git a/gms-client/gms-client-lib/pom.xml b/gms-client/gms-client-lib/pom.xml
index 7ef5181..b05f489 100644
--- a/gms-client/gms-client-lib/pom.xml
+++ b/gms-client/gms-client-lib/pom.xml
@@ -7,21 +7,10 @@
     <packaging>jar</packaging>
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <maven.compiler.source>1.8</maven.compiler.source>
-        <maven.compiler.target>1.8</maven.compiler.target>
-        <spring.version>5.1.8.RELEASE</spring.version>
+        <maven.compiler.source>12</maven.compiler.source>
+        <maven.compiler.target>12</maven.compiler.target>
     </properties>
     <dependencies>
-        <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-web</artifactId>
-            <version>${spring.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-databind</artifactId>
-            <version>2.9.9</version>
-        </dependency>
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
index ab11cee..eb46bfc 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
@@ -1,134 +1,69 @@
 package it.inaf.ia2.gms.client;
 
-import it.inaf.ia2.gms.client.model.Group;
-import it.inaf.ia2.gms.client.model.Member;
+import it.inaf.ia2.gms.client.call.HttpClientWrapper;
+import it.inaf.ia2.gms.client.call.AddMemberCall;
+import it.inaf.ia2.gms.client.call.AddPermissionCall;
+import it.inaf.ia2.gms.client.call.CreateGroupCall;
+import it.inaf.ia2.gms.client.call.DeleteGroupCall;
+import it.inaf.ia2.gms.client.call.GetUserGroupsCall;
+import it.inaf.ia2.gms.client.call.ListGroupsCall;
+import it.inaf.ia2.gms.client.call.RemoveMemberCall;
+import it.inaf.ia2.gms.client.call.RemovePermissionCall;
 import it.inaf.ia2.gms.client.model.Permission;
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.util.UriComponentsBuilder;
 
 public class GmsClient {
 
-    private final String baseUrl;
-    private final String authHeader;
-    private RestTemplate restTemplate;
+    HttpClientWrapper httpClientWrapper;
 
-    public GmsClient(String baseUrl, String clientId, String clientSecret) {
+    public GmsClient(String baseUrl) {
 
         if (!baseUrl.endsWith("/")) {
             baseUrl += "/";
         }
-        baseUrl += "ws/basic";
+        baseUrl += "ws/jwt";
 
-        this.baseUrl = baseUrl;
-
-        String auth = clientId + ":" + clientSecret;
-        byte[] encodedAuth = Base64.getEncoder().encode(
-                auth.getBytes(StandardCharsets.UTF_8));
-        authHeader = "Basic " + new String(encodedAuth);
-
-        restTemplate = new RestTemplate();
+        httpClientWrapper = new HttpClientWrapper(baseUrl);
     }
 
-    /**
-     * For testing purpose.
-     */
-    protected void setRestTemplate(RestTemplate restTemplate) {
-        this.restTemplate = restTemplate;
+    public GmsClient setAccessToken(String accessToken) {
+        httpClientWrapper.setAccessToken(accessToken);
+        return this;
     }
 
-    public Group addGroup(List<String> names) {
-
-        String url = UriComponentsBuilder.fromHttpUrl(baseUrl)
-                .pathSegment("group")
-                .toUriString();
-
-        HttpEntity<List<String>> httpEntity = getEntity(names);
-
-        return restTemplate.exchange(url, HttpMethod.POST, httpEntity, Group.class).getBody();
+    public List<String> getMyGroups(String prefix) {
+        return new GetUserGroupsCall(httpClientWrapper).getUserGroups(prefix);
     }
 
-    public void removeGroup(List<String> names) {
-
-        String url = UriComponentsBuilder.fromHttpUrl(baseUrl)
-                .pathSegment("group")
-                .queryParam("names", names.toArray())
-                .toUriString();
-
-        restTemplate.exchange(url, HttpMethod.DELETE, getEntity(), Void.class);
+    public List<String> listGroups(String prefix) {
+        return new ListGroupsCall(httpClientWrapper).listGroups(prefix);
     }
 
-    public Member addMember(List<String> names, String userId) {
-
-        String url = UriComponentsBuilder.fromHttpUrl(baseUrl)
-                .pathSegment("member")
-                .toUriString();
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("names", names);
-        params.put("userId", userId);
-        HttpEntity<Map<String, Object>> httpEntity = getEntity(params);
-
-        return restTemplate.exchange(url, HttpMethod.POST, httpEntity, Member.class).getBody();
+    public List<String> getUserGroups(String userId, String prefix) {
+        return new GetUserGroupsCall(httpClientWrapper).getUserGroups(userId, prefix);
     }
 
-    public void removeMember(List<String> names, String userId) {
-
-        String url = UriComponentsBuilder.fromHttpUrl(baseUrl)
-                .pathSegment("member")
-                .queryParam("names", names.toArray())
-                .queryParam("userId", userId)
-                .toUriString();
-
-        restTemplate.exchange(url, HttpMethod.DELETE, getEntity(), Void.class);
+    public void createGroup(String completeGroupName, boolean leaf) {
+        new CreateGroupCall(httpClientWrapper).createGroup(completeGroupName, leaf);
     }
 
-    public Permission addPermission(List<String> names, String userId, String permission) {
-
-        String url = UriComponentsBuilder.fromHttpUrl(baseUrl)
-                .pathSegment("permission")
-                .toUriString();
-
-        Map<String, Object> params = new HashMap<>();
-        params.put("names", names);
-        params.put("userId", userId);
-        params.put("permission", permission);
-        HttpEntity<Map<String, Object>> httpEntity = getEntity(params);
-
-        return restTemplate.exchange(url, HttpMethod.POST, httpEntity, Permission.class).getBody();
+    public void deleteGroup(String completeGroupName) {
+        new DeleteGroupCall(httpClientWrapper).deleteGroup(completeGroupName);
     }
 
-    public void removePermission(List<String> names, String userId) {
-
-        String url = UriComponentsBuilder.fromHttpUrl(baseUrl)
-                .pathSegment("permission")
-                .queryParam("names", names.toArray())
-                .queryParam("userId", userId)
-                .toUriString();
-
-        restTemplate.exchange(url, HttpMethod.DELETE, getEntity(), Void.class);
+    public void addMember(String completeGroupName, String userId) {
+        new AddMemberCall(httpClientWrapper).addMember(completeGroupName, userId);
     }
 
-    private HttpEntity<?> getEntity() {
-        return new HttpEntity<>(getHeaders());
+    public void removeMember(String completeGroupName, String userId) {
+        new RemoveMemberCall(httpClientWrapper).removeMember(completeGroupName, userId);
     }
 
-    private <T> HttpEntity<T> getEntity(T body) {
-        return new HttpEntity<>(body, getHeaders());
+    public void addPermission(String completeGroupName, String userId, Permission permission) {
+        new AddPermissionCall(httpClientWrapper).addPermission(completeGroupName, userId, permission);
     }
 
-    private HttpHeaders getHeaders() {
-        return new HttpHeaders() {
-            {
-                set("Authorization", authHeader);
-            }
-        };
+    public void removePermission(String completeGroupName, String userId) {
+        new RemovePermissionCall(httpClientWrapper).removePermission(completeGroupName, userId);
     }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
new file mode 100644
index 0000000..42abfe7
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
@@ -0,0 +1,34 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+
+public class AddMemberCall extends BaseGmsCall {
+
+    public AddMemberCall(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    public boolean addMember(String completeGroupName, String userId) {
+
+        String endpoint = "membership";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+
+        HttpRequest groupsRequest = newHttpRequest(endpoint)
+                .header("Accept", "text/plain")
+                .POST(BodyPublishers.ofString("user_id=" + userId))
+                .build();
+
+        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 200) {
+                        return true;
+                    }
+                    logServerErrorInputStream(groupsRequest, response);
+                    throw new IllegalStateException("Unable to add member to group");
+                }).join();
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
new file mode 100644
index 0000000..99c5d80
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
@@ -0,0 +1,39 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.model.Permission;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+
+public class AddPermissionCall extends BaseGmsCall {
+
+    public AddPermissionCall(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    public boolean addPermission(String completeGroupName, String userId, Permission permission) {
+
+        String endpoint = "permission";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+
+        BodyPublisher requestBody = BodyPublishers.ofString(
+                "user_id=" + userId + "&permission=" + permission);
+
+        HttpRequest groupsRequest = newHttpRequest(endpoint)
+                .header("Accept", "text/plain")
+                .POST(requestBody)
+                .build();
+
+        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 200) {
+                        return true;
+                    }
+                    logServerErrorInputStream(groupsRequest, response);
+                    throw new IllegalStateException("Unable to add permission");
+                }).join();
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java
new file mode 100644
index 0000000..6f03160
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java
@@ -0,0 +1,45 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.io.InputStream;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse;
+import java.util.Scanner;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public abstract class BaseGmsCall {
+
+    private static final Logger LOGGER = Logger.getLogger(BaseGmsCall.class.getName());
+
+    protected final HttpClientWrapper clientWrapper;
+
+    public BaseGmsCall(HttpClientWrapper clientWrapper) {
+        this.clientWrapper = clientWrapper;
+    }
+
+    protected HttpClient getClient() {
+        return clientWrapper.getClient();
+    }
+
+    protected Builder newHttpRequest(String endpoint) {
+        return clientWrapper.newHttpRequest(endpoint);
+    }
+
+    protected void logServerError(HttpRequest request, HttpResponse<String> response) {
+        LOGGER.log(Level.SEVERE, () -> "Error while reading " + request.uri()
+                + "\nServer response status code is " + response.statusCode()
+                + "\nAServer response text is " + response.body());
+    }
+
+    protected void logServerErrorInputStream(HttpRequest request, HttpResponse<InputStream> response) {
+        LOGGER.log(Level.SEVERE, () -> {
+            Scanner s = new Scanner(response.body()).useDelimiter("\\A");
+            String responseBody = s.hasNext() ? s.next() : "";
+            return "Error while reading " + request.uri()
+                    + "\nServer response status code is " + response.statusCode()
+                    + "\nServer response text is " + responseBody;
+        });
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
new file mode 100644
index 0000000..9636232
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
@@ -0,0 +1,29 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+
+public class CreateGroupCall extends BaseGmsCall {
+
+    public CreateGroupCall(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    public boolean createGroup(String completeGroupName, boolean leaf) {
+
+        HttpRequest groupsRequest = newHttpRequest(completeGroupName)
+                .header("Accept", "text/plain")
+                .POST(BodyPublishers.ofString("leaf=" + leaf))
+                .build();
+
+        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 200) {
+                        return true;
+                    }
+                    logServerErrorInputStream(groupsRequest, response);
+                    throw new IllegalStateException("Unable to create group");
+                }).join();
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
new file mode 100644
index 0000000..56b7b98
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
@@ -0,0 +1,28 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+public class DeleteGroupCall extends BaseGmsCall {
+
+    public DeleteGroupCall(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    public boolean deleteGroup(String completeGroupName) {
+
+        HttpRequest groupsRequest = newHttpRequest(completeGroupName)
+                .header("Accept", "text/plain")
+                .DELETE()
+                .build();
+
+        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 204) {
+                        return true;
+                    }
+                    logServerErrorInputStream(groupsRequest, response);
+                    throw new IllegalStateException("Unable to delete group");
+                }).join();
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
new file mode 100644
index 0000000..57ab0ff
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
@@ -0,0 +1,91 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+public class GetUserGroupsCall extends BaseGmsCall {
+
+    public GetUserGroupsCall(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    /**
+     * Returns the groups the user belongs to. If a groupsPrefix (parent group)
+     * is specified, the prefix is removed from the list.
+     */
+    public List<String> getUserGroups(String prefix) {
+
+        List<String> groups = new ArrayList<>();
+
+        HttpRequest groupsRequest = newHttpRequest("search")
+                .header("Accept", "text/plain")
+                .GET()
+                .build();
+
+        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 200) {
+                        return response.body();
+                    }
+                    logServerErrorInputStream(groupsRequest, response);
+                    throw new IllegalStateException("Unable to retrieve groups");
+                })
+                .thenApply(inputStream -> {
+                    try (Scanner scan = new Scanner(inputStream)) {
+                        while (scan.hasNextLine()) {
+                            String line = scan.nextLine();
+                            if (!line.isEmpty()) {
+                                if (prefix == null || prefix.isEmpty()) {
+                                    groups.add(line);
+                                } else {
+                                    if (line.startsWith(prefix)) {
+                                        line = line.substring(prefix.length());
+                                        groups.add(line);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    return groups;
+                }).join();
+    }
+
+    public List<String> getUserGroups(String userId, String prefix) {
+
+        List<String> groups = new ArrayList<>();
+
+        String endpoint = "membership";
+        if (prefix != null && !prefix.isBlank()) {
+            endpoint += "/" + prefix;
+        }
+        endpoint += "?user_id=" + userId;
+
+        HttpRequest groupsRequest = newHttpRequest(endpoint)
+                .header("Accept", "text/plain")
+                .GET()
+                .build();
+
+        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 200) {
+                        return response.body();
+                    }
+                    logServerErrorInputStream(groupsRequest, response);
+                    throw new IllegalStateException("Unable to retrieve groups");
+                })
+                .thenApply(inputStream -> {
+                    try (Scanner scan = new Scanner(inputStream)) {
+                        while (scan.hasNextLine()) {
+                            String line = scan.nextLine();
+                            if (!line.isEmpty()) {
+                                groups.add(line);
+                            }
+                        }
+                    }
+                    return groups;
+                }).join();
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java
new file mode 100644
index 0000000..7bbf781
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java
@@ -0,0 +1,41 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.Builder;
+
+public class HttpClientWrapper {
+
+    private final String baseGmsUri;
+    private final HttpClient client;
+
+    private String accessToken;
+
+    public HttpClientWrapper(String baseGmsUri) {
+        String uri = baseGmsUri;
+        if (!uri.endsWith("/")) {
+            uri += "/";
+        }
+        this.baseGmsUri = uri;
+
+        this.client = HttpClient.newBuilder()
+                .followRedirects(HttpClient.Redirect.ALWAYS)
+                .build();
+    }
+
+    public HttpClientWrapper setAccessToken(String accessToken) {
+        this.accessToken = accessToken;
+        return this;
+    }
+
+    Builder newHttpRequest(String endpoint) {
+        return HttpRequest.newBuilder()
+                .uri(URI.create(baseGmsUri + endpoint))
+                .header("Authorization", "Bearer " + accessToken);
+    }
+
+    HttpClient getClient() {
+        return client;
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
new file mode 100644
index 0000000..7170eba
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
@@ -0,0 +1,54 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+public class ListGroupsCall extends BaseGmsCall {
+
+    public ListGroupsCall(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    /**
+     * Returns the list of the groups in a given parent group (if the user has
+     * the privileges to see that information). The prefix is removed by the
+     * service.
+     */
+    public List<String> listGroups(String prefix) {
+
+        List<String> groups = new ArrayList<>();
+
+        String uri = "list";
+        if (prefix != null && !prefix.isBlank()) {
+            uri += "/" + prefix;
+        }
+
+        HttpRequest groupsRequest = newHttpRequest(uri)
+                .header("Accept", "text/plain")
+                .GET()
+                .build();
+
+        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 200) {
+                        return response.body();
+                    }
+                    logServerErrorInputStream(groupsRequest, response);
+                    throw new IllegalStateException("Unable to list groups");
+                })
+                .thenApply(inputStream -> {
+                    try (Scanner scan = new Scanner(inputStream)) {
+                        while (scan.hasNextLine()) {
+                            String line = scan.nextLine();
+                            if (!line.isEmpty()) {
+                                groups.add(line);
+                            }
+                        }
+                    }
+                    return groups;
+                }).join();
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
new file mode 100644
index 0000000..cc5b01a
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
@@ -0,0 +1,34 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+public class RemoveMemberCall extends BaseGmsCall {
+
+    public RemoveMemberCall(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    public boolean removeMember(String completeGroupName, String userId) {
+
+        String endpoint = "membership";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+        endpoint += "?user_id=" + userId;
+
+        HttpRequest groupsRequest = newHttpRequest(endpoint)
+                .header("Accept", "text/plain")
+                .DELETE()
+                .build();
+
+        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 204) {
+                        return true;
+                    }
+                    logServerErrorInputStream(groupsRequest, response);
+                    throw new IllegalStateException("Unable to remove member from group");
+                }).join();
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
new file mode 100644
index 0000000..7f17e76
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
@@ -0,0 +1,35 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+public class RemovePermissionCall extends BaseGmsCall {
+
+    public RemovePermissionCall(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    public boolean removePermission(String completeGroupName, String userId) {
+
+        String endpoint = "permission";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+        endpoint += "?user_id=" + userId;
+
+        HttpRequest groupsRequest = newHttpRequest(endpoint)
+                .header("Accept", "text/plain")
+                .DELETE()
+                .build();
+
+        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 204) {
+                        return true;
+                    }
+                    logServerErrorInputStream(groupsRequest, response);
+                    throw new IllegalStateException("Unable to remove permission");
+                }).join();
+    }
+
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Group.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Group.java
deleted file mode 100644
index d0b95fd..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Group.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package it.inaf.ia2.gms.client.model;
-
-public class Group {
-
-    private String id;
-    private String name;
-    private String path;
-    private String parentPath;
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public String getPath() {
-        return path;
-    }
-
-    public void setPath(String path) {
-        this.path = path;
-    }
-
-    public String getParentPath() {
-        return parentPath;
-    }
-
-    public void setParentPath(String parentPath) {
-        this.parentPath = parentPath;
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Member.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Member.java
deleted file mode 100644
index 37b0096..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Member.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package it.inaf.ia2.gms.client.model;
-
-public class Member {
-
-    private String groupId;
-    private String userId;
-
-    public String getGroupId() {
-        return groupId;
-    }
-
-    public void setGroupId(String groupId) {
-        this.groupId = groupId;
-    }
-
-    public String getUserId() {
-        return userId;
-    }
-
-    public void setUserId(String userId) {
-        this.userId = userId;
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Permission.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Permission.java
index bb809c4..71e708c 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Permission.java
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Permission.java
@@ -1,32 +1,9 @@
 package it.inaf.ia2.gms.client.model;
 
-public class Permission {
+public enum Permission {
 
-    private String groupId;
-    private String userId;
-    private String permission;
-
-    public String getGroupId() {
-        return groupId;
-    }
-
-    public void setGroupId(String groupId) {
-        this.groupId = groupId;
-    }
-
-    public String getUserId() {
-        return userId;
-    }
-
-    public void setUserId(String userId) {
-        this.userId = userId;
-    }
-
-    public String getPermission() {
-        return permission;
-    }
-
-    public void setPermission(String permission) {
-        this.permission = permission;
-    }
+    ADMIN,
+    MANAGE_MEMBERS,
+    VIEW_MEMBERS,
+    TRAVERSE;
 }
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
index e0eeb8d..63233fb 100644
--- a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
+++ b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
@@ -1,161 +1,182 @@
 package it.inaf.ia2.gms.client;
 
-import it.inaf.ia2.gms.client.model.Group;
-import it.inaf.ia2.gms.client.model.Member;
+import it.inaf.ia2.gms.client.call.HttpClientWrapper;
+import it.inaf.ia2.gms.client.call.MockedHttpClientWrapper;
 import it.inaf.ia2.gms.client.model.Permission;
-import java.util.Arrays;
-import java.util.HashMap;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
-import java.util.Map;
+import java.util.concurrent.CompletableFuture;
 import static org.junit.Assert.assertEquals;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.mockito.ArgumentMatchers;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import org.mockito.junit.MockitoJUnitRunner;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.client.RestTemplate;
 
 @RunWith(MockitoJUnitRunner.class)
 public class GmsClientTest {
 
     private static final String BASE_URL = "http://base-url";
 
-    private RestTemplate restTemplate;
+    private HttpClient httpClient;
     private GmsClient client;
 
     @Before
     public void setUp() {
 
-        restTemplate = mock(RestTemplate.class);
+        httpClient = mock(HttpClient.class);
 
-        client = new GmsClient(BASE_URL, "test", "test");
-        client.setRestTemplate(restTemplate);
+        HttpClientWrapper clientWrapper = new MockedHttpClientWrapper(BASE_URL, httpClient);
 
-        when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(HttpEntity.class), any(Class.class)))
-                .thenReturn(new ResponseEntity<>(HttpStatus.I_AM_A_TEAPOT));
+        client = new GmsClient(BASE_URL);
+        client.httpClientWrapper = clientWrapper;
     }
 
     @Test
-    public void testCreateGroup() {
+    public void testGetMyGroups() {
+
+        String body = "LBT.INAF\n"
+                + "LBT.AZ";
 
-        List<String> names = Arrays.asList("LBT", "INAF");
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200, body));
 
-        client.addGroup(names);
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        List<String> groups = client.getMyGroups("LBT.");
 
-        ArgumentCaptor<HttpEntity> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate, times(1)).exchange(eq(BASE_URL + "/ws/group"),
-                eq(HttpMethod.POST), entityCaptor.capture(), eq(Group.class));
+        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "search"), any());
 
-        HttpEntity<?> entity = entityCaptor.getValue();
-        verifyAuthHeaders(entity);
-        verifyBody(entity, names);
+        assertEquals(2, groups.size());
+        assertEquals("INAF", groups.get(0));
+        assertEquals("AZ", groups.get(1));
     }
 
     @Test
-    public void testDeleteGroup() {
+    public void testListGroups() {
+
+        String body = "INAF\n"
+                + "AZ";
 
-        List<String> names = Arrays.asList("LBT", "INAF");
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200, body));
 
-        client.removeGroup(names);
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        List<String> groups = client.listGroups("LBT.");
 
-        ArgumentCaptor<HttpEntity> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate, times(1)).exchange(eq(BASE_URL + "/ws/group?names=LBT&names=INAF"),
-                eq(HttpMethod.DELETE), entityCaptor.capture(), eq(Void.class));
+        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "list/LBT."), any());
 
-        HttpEntity<?> entity = entityCaptor.getValue();
-        verifyAuthHeaders(entity);
+        assertEquals(2, groups.size());
+        assertEquals("INAF", groups.get(0));
+        assertEquals("AZ", groups.get(1));
     }
 
     @Test
-    public void testAddMember() {
+    public void testCreateGroup() {
 
-        List<String> names = Arrays.asList("LBT", "INAF");
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
 
-        client.addMember(names, "user_id");
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        client.createGroup("LBT.INAF", false);
 
-        ArgumentCaptor<HttpEntity> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate, times(1)).exchange(eq(BASE_URL + "/ws/member"),
-                eq(HttpMethod.POST), entityCaptor.capture(), eq(Member.class));
+        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "LBT.INAF"), any());
+    }
 
-        HttpEntity<?> entity = entityCaptor.getValue();
-        verifyAuthHeaders(entity);
+    @Test
+    public void testDeleteGroup() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
 
-        Map<String, Object> expectedBody = new HashMap<>();
-        expectedBody.put("names", names);
-        expectedBody.put("userId", "user_id");
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        client.deleteGroup("LBT.INAF");
 
-        verifyBody(entity, expectedBody);
+        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "LBT.INAF"), any());
     }
 
     @Test
-    public void testRemoveMember() {
-
-        List<String> names = Arrays.asList("LBT", "INAF");
+    public void testAddMember() {
 
-        client.removeMember(names, "user_id");
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
 
-        ArgumentCaptor<HttpEntity> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate, times(1)).exchange(eq(BASE_URL + "/ws/member?names=LBT&names=INAF&userId=user_id"),
-                eq(HttpMethod.DELETE), entityCaptor.capture(), eq(Void.class));
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        client.addMember("LBT.INAF", "user");
 
-        HttpEntity<?> entity = entityCaptor.getValue();
-        verifyAuthHeaders(entity);
+        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "membership/LBT.INAF"), any());
     }
 
     @Test
-    public void testAddPermission() {
+    public void testRemoveMember() {
 
-        List<String> names = Arrays.asList("LBT", "INAF");
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
 
-        client.addPermission(names, "user_id", "ADMIN");
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        client.removeMember("LBT.INAF", "user");
 
-        ArgumentCaptor<HttpEntity> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate, times(1)).exchange(eq(BASE_URL + "/ws/permission"),
-                eq(HttpMethod.POST), entityCaptor.capture(), eq(Permission.class));
+        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "membership/LBT.INAF?user_id=user"), any());
+    }
 
-        HttpEntity<?> entity = entityCaptor.getValue();
-        verifyAuthHeaders(entity);
+    @Test
+    public void testAddPermission() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
 
-        Map<String, Object> expectedBody = new HashMap<>();
-        expectedBody.put("names", names);
-        expectedBody.put("userId", "user_id");
-        expectedBody.put("permission", "ADMIN");
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        client.addPermission("LBT.INAF", "user", Permission.ADMIN);
 
-        verifyBody(entity, expectedBody);
+        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "permission/LBT.INAF"), any());
     }
 
     @Test
-    public void testDeletePermission() {
+    public void testRemovePermission() {
 
-        List<String> names = Arrays.asList("LBT", "INAF");
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
 
-        client.removePermission(names, "user_id");
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        client.removePermission("LBT.INAF", "user");
 
-        ArgumentCaptor<HttpEntity> entityCaptor = ArgumentCaptor.forClass(HttpEntity.class);
-        verify(restTemplate, times(1)).exchange(eq(BASE_URL + "/ws/permission?names=LBT&names=INAF&userId=user_id"),
-                eq(HttpMethod.DELETE), entityCaptor.capture(), eq(Void.class));
+        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "permission/LBT.INAF?user_id=user"), any());
+    }
 
-        HttpEntity<?> entity = entityCaptor.getValue();
-        verifyAuthHeaders(entity);
+    private HttpResponse getMockedResponse(int statusCode, String body) {
+        HttpResponse response = getMockedResponse(statusCode);
+        InputStream in = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
+        when(response.body()).thenReturn(in);
+        return response;
     }
 
-    private void verifyAuthHeaders(HttpEntity<?> entity) {//
-        String authHeader = entity.getHeaders().getFirst("Authorization");
-        assertEquals("Basic dGVzdDp0ZXN0", authHeader);
+    private HttpResponse getMockedResponse(int statusCode) {
+        HttpResponse response = mock(HttpResponse.class);
+        when(response.statusCode()).thenReturn(statusCode);
+        return response;
     }
 
-    private <T> void verifyBody(HttpEntity<?> entity, T body) {
-        assertEquals(entity.getBody(), body);
+    private HttpRequest endpointEq(String expectedMethod, String expectedEndpoint) {
+        return ArgumentMatchers.argThat(endpointEqArgumentMatcher(expectedMethod, expectedEndpoint));
+    }
+
+    private ArgumentMatcher<HttpRequest> endpointEqArgumentMatcher(String expectedMethod, String expectedEndpoint) {
+
+        return new ArgumentMatcher<HttpRequest>() {
+
+            private final String expectedUri = BASE_URL + "/" + expectedEndpoint;
+
+            @Override
+            public boolean matches(HttpRequest request) {
+                return expectedMethod.equals(request.method()) && expectedUri.equals(request.uri().toString());
+            }
+
+            @Override
+            public String toString() {
+                return expectedMethod + " " + expectedUri;
+            }
+        };
     }
 }
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java
new file mode 100644
index 0000000..d287f90
--- /dev/null
+++ b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java
@@ -0,0 +1,18 @@
+package it.inaf.ia2.gms.client.call;
+
+import java.net.http.HttpClient;
+
+public class MockedHttpClientWrapper extends HttpClientWrapper {
+
+    private final HttpClient mockedClient;
+
+    public MockedHttpClientWrapper(String baseGmsUri, HttpClient mockedClient) {
+        super(baseGmsUri);
+        this.mockedClient = mockedClient;
+    }
+
+    @Override
+    HttpClient getClient() {
+        return mockedClient;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
index c3e6d98..f338cfe 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
@@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -174,6 +175,13 @@ public class JWTWebServiceController {
         }
     }
 
+    @DeleteMapping(value = "/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
+    public void deleteGroup(@PathVariable("group") String groupParam, HttpServletResponse response) {
+        GroupEntity group = getGroupFromNames(extractGroupNames(groupParam));
+        groupsDAO.deleteGroupById(group.getId());
+        response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+    }
+
     @GetMapping(value = {"/membership/{group:.+}", "/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
     public void getMembership(@PathVariable("group") Optional<String> group, @RequestParam("user_id") String userId, HttpServletResponse response) throws IOException {
 
@@ -202,6 +210,16 @@ public class JWTWebServiceController {
         membershipManager.addMember(groupEntity, targetUserId);
     }
 
+    @DeleteMapping(value = {"/membership/{group:.+}", "/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
+    public void removeMember(@PathVariable("group") Optional<String> group, @RequestParam("user_id") String userId,
+            HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(group));
+        membershipManager.removeMember(groupEntity, userId);
+
+        response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+    }
+
     @PostMapping(value = {"/permission/{group:.+}", "/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE)
     public void addPermission(@PathVariable("group") Optional<String> groupNames, HttpServletRequest request, HttpServletResponse response) throws IOException {
 
@@ -221,6 +239,16 @@ public class JWTWebServiceController {
         permissionsManager.addPermission(groupEntity, targetUserId, Permission.valueOf(permissionParam));
     }
 
+    @DeleteMapping(value = {"/permission/{group:.+}", "/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE)
+    public void removePermission(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") String userId,
+            HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
+        permissionsManager.removePermission(groupEntity, userId);
+
+        response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+    }
+
     private GroupEntity getGroupFromNames(List<String> groupNames) {
         if (groupNames.isEmpty()) {
             return getRoot();
-- 
GitLab