From c67052b0702d5a2b0be398f3e8831a4e1596ae29 Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Fri, 21 Aug 2020 18:27:03 +0200
Subject: [PATCH] Added endpoint for retrieving member email addresses; CLI
 improvements

---
 gms-client/gms-cli/pom.xml                    |   1 +
 .../main/java/it/inaf/ia2/gms/cli/CLI.java    | 185 ++++++++++++++----
 .../it/inaf/ia2/gms/client/GmsClient.java     |  16 +-
 .../inaf/ia2/gms/client/GmsClientBuilder.java |  40 ++++
 .../inaf/ia2/gms/client/call/BaseGmsCall.java |   6 +-
 .../client/call/GetMemberEmailAddresses.java  |  50 +++++
 .../gms/client/call/HttpClientWrapper.java    |  64 ++++++
 .../it/inaf/ia2/gms/client/GmsClientTest.java |   6 +-
 .../client/call/HttpClientWrapperTest.java    |  15 ++
 gms/pom.xml                                   |   1 +
 .../controller/JWTWebServiceController.java   |  28 +++
 .../ia2/gms/manager/MembershipManager.java    |   6 +-
 .../it/inaf/ia2/gms/model/Permission.java     |  21 ++
 .../java/it/inaf/ia2/gms/model/RapUser.java   |  10 +-
 gms/src/main/resources/application.properties |   2 +-
 .../it/inaf/ia2/gms/model/PermissionTest.java |  30 +++
 16 files changed, 426 insertions(+), 55 deletions(-)
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java
 create mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
 create mode 100644 gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java

diff --git a/gms-client/gms-cli/pom.xml b/gms-client/gms-cli/pom.xml
index 76393e0..cfaad41 100644
--- a/gms-client/gms-cli/pom.xml
+++ b/gms-client/gms-cli/pom.xml
@@ -22,6 +22,7 @@
         </dependency>
     </dependencies>
     <build>
+        <finalName>gms-cli</finalName>
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
diff --git a/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java b/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
index f522fcd..a237abc 100644
--- a/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
+++ b/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
@@ -1,95 +1,207 @@
 package it.inaf.ia2.gms.cli;
 
 import it.inaf.ia2.gms.client.GmsClient;
+import it.inaf.ia2.gms.client.GmsClientBuilder;
 import it.inaf.ia2.gms.client.model.Permission;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.util.List;
 import java.util.Properties;
 
 public class CLI {
 
-    private final GmsClient client;
-
     public static void main(String[] args) throws Exception {
-        new CLI().run(args);
+        new CLI(args).run();
+    }
+
+    private final String args[];
+    private int argIndex;
+
+    private String gmsBaseUrl;
+    private String rapBaseUrl;
+    private String clientId;
+    private String clientSecret;
+    private String token;
+
+    private GmsClient client;
+
+    private CLI(String... args) {
+        this.args = args;
+    }
+
+    private void run() throws Exception {
+        if (args.length < 2) {
+            displayUsage();
+        }
+
+        boolean commandParsed = false;
+        while (argIndex < args.length && !commandParsed) {
+            switch (args[argIndex]) {
+                case "--config-file":
+                    loadConfigFromFile(new File(getNextArg()));
+                    break;
+                case "--token-file":
+                    loadTokenFromFile(new File(getNextArg()));
+                    break;
+                case "--gms-url":
+                    gmsBaseUrl = getNextArg();
+                    break;
+                case "--rap-url":
+                    rapBaseUrl = getNextArg();
+                    break;
+                case "--client-id":
+                    clientId = getNextArg();
+                    break;
+                case "--client-secret":
+                    clientSecret = getNextArg();
+                    break;
+                default:
+                    verifyConfigLoaded();
+                    createClient();
+                    parseCommand();
+                    commandParsed = true;
+                    break;
+            }
+            argIndex++;
+        }
+    }
+
+    private String getNextArg() {
+        if (argIndex + 1 == args.length) {
+            System.err.println("Missing value for option " + args[argIndex]);
+            System.exit(1);
+        }
+        return args[++argIndex];
     }
 
-    private CLI() throws IOException {
+    private void verifyConfigLoaded() {
+
+        if (gmsBaseUrl == null) {
+            // Attempt reading gms.properties in current directory
+            loadConfigFromFile(new File("gms.properties"));
+        }
+
+        if (clientId == null && token == null) {
+            // Attempt loading token.txt in current directory
+            loadTokenFromFile(new File("token.txt"));
+        }
+
+        if (token != null && (clientSecret == null || rapBaseUrl == null)) {
+            System.err.println("Client secret and RAP base URL not configured");
+            System.exit(1);
+        }
+    }
+
+    private void createClient() {
+        GmsClientBuilder clientBuilder = new GmsClientBuilder()
+                .setGmsBaseUrl(gmsBaseUrl);
+
+        if (token != null) {
+            client = clientBuilder.build();
+            client.setAccessToken(token);
+        } else {
+            client = clientBuilder.setClientId(clientId)
+                    .setClientSecret(clientSecret)
+                    .setRapBaseUrl(rapBaseUrl)
+                    .build();
+        }
+    }
+
+    private void loadConfigFromFile(File config) {
 
-        File config = new File("gms.properties");
         if (!config.exists()) {
-            System.err.println("Unable to find the file gms.properties");
+            System.err.println("Config file " + config.getAbsolutePath() + " doesn't exist");
             System.exit(1);
         }
 
         Properties properties = new Properties();
         try (InputStream in = new FileInputStream(config)) {
             properties.load(in);
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
         }
 
-        String baseUrl = (String) properties.get("base_url");
-        if (baseUrl == null) {
-            System.err.println("Missing base_url in gms.properties");
+        gmsBaseUrl = properties.getProperty("gms_url");
+        if (gmsBaseUrl == null) {
+            System.err.println("Missing gms_url in gms.properties");
             System.exit(1);
         }
+        rapBaseUrl = properties.getProperty("rap_url");
+        clientId = properties.getProperty("client_id");
+        clientSecret = properties.getProperty("client_secret");
+    }
+
+    private void loadTokenFromFile(File tokenFile) {
 
-        String token = (String) properties.get("token");
-        if (token == null) {
-            System.err.println("Missing token in gms.properties");
+        if (!tokenFile.exists()) {
+            System.err.println("Token file " + tokenFile.getAbsolutePath() + " doesn't exist");
             System.exit(1);
         }
 
-        client = new GmsClient(baseUrl).setAccessToken(token);
+        try (InputStream in = new FileInputStream(tokenFile)) {
+            java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\A");
+            token = s.next().trim();
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
     }
 
-    public void run(String... args) throws Exception {
-        if (args.length < 2) {
-            displayUsage();
-        }
+    private void parseCommand() {
 
-        switch (args[0]) {
+        switch (args[argIndex]) {
             case "create-group":
                 boolean leaf = false;
-                if (args.length > 1) {
-                    leaf = Boolean.parseBoolean(args[2]);
+                if (argIndex + 2 < args.length) {
+                    leaf = Boolean.parseBoolean(args[argIndex + 2]);
                 }
-                client.createGroup(args[1], leaf);
+                client.createGroup(args[argIndex + 1], leaf);
                 System.out.println("Group created");
                 break;
             case "delete-group":
-                client.deleteGroup(args[1]);
+                client.deleteGroup(args[argIndex + 1]);
                 System.out.println("Group deleted");
                 break;
             case "add-member":
-                if (args.length < 3) {
+                if (argIndex + 2 >= args.length) {
                     displayUsage();
                 }
-                client.addMember(args[1], args[2]);
+                client.addMember(args[argIndex + 1], args[argIndex + 2]);
                 System.out.println("Member added");
                 break;
             case "remove-member":
-                if (args.length < 3) {
+                if (argIndex + 2 >= args.length) {
                     displayUsage();
                 }
-                client.removeMember(args[1], args[2]);
+                client.removeMember(args[argIndex + 1], args[argIndex + 2]);
                 System.out.println("Member removed");
                 break;
             case "add-permission":
-                if (args.length < 4) {
+                if (argIndex + 3 >= args.length) {
                     displayUsage();
                 }
-                client.addPermission(args[1], args[2], Permission.valueOf(args[3]));
+                client.addPermission(args[argIndex + 1], args[argIndex + 2], Permission.valueOf(args[argIndex + 3]));
                 System.out.println("Permission added");
                 break;
             case "delete-permission":
-                if (args.length < 4) {
+                if (argIndex + 2 >= args.length) {
                     displayUsage();
                 }
-                client.removePermission(args[1], args[2]);
+                client.removePermission(args[argIndex + 1], args[argIndex + 2]);
                 System.out.println("Permission removed");
                 break;
+            case "get-member-email-addresses":
+                Permission permission = null;
+                if (argIndex + 2 < args.length) {
+                    permission = Permission.valueOf(args[argIndex + 2]);
+                }
+                List<String> addresses = client.getMemberEmailAddresses(args[argIndex + 1], permission);
+                for (String address : addresses) {
+                    System.out.println(address);
+                }
+                break;
             default:
                 displayUsage();
                 break;
@@ -97,13 +209,20 @@ public class CLI {
     }
 
     private void displayUsage() {
-        System.out.println("java -jar gms-client.jar\n"
-                + "    create-group <name1.name2.name3> <leaf>\n"
+        System.out.println("gms-client\n"
+                + "    [--config-file <file>]\n"
+                + "    [--token-file <file>]\n"
+                + "    [--gms-url <url>]\n"
+                + "    [--rap-url <url>]\n"
+                + "    [--client-id <id>]\n"
+                + "    [--client-secret <secret>]\n"
+                + "    create-group <name1.name2.name3> [<leaf>]\n"
                 + "    delete-group <name1.name2.name3>\n"
                 + "    add-member <name1.name2.name3> <user_id>\n"
                 + "    remove-member <name1.name2.name3> <user_id>\n"
                 + "    add-permission <name1.name2.name3> <user_id> <permission>\n"
-                + "    delete-permission <name1.name2.name3> <user_id>");
+                + "    delete-permission <name1.name2.name3> <user_id>\n"
+                + "    get-member-email-addresses <name1.name2.name3> [<permission>]");
         System.exit(0);
     }
 }
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 7792277..86a9100 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
@@ -7,6 +7,7 @@ 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.GetGroupPermissionsCall;
+import it.inaf.ia2.gms.client.call.GetMemberEmailAddresses;
 import it.inaf.ia2.gms.client.call.GetUserGroupsCall;
 import it.inaf.ia2.gms.client.call.GetUserPermissionsCall;
 import it.inaf.ia2.gms.client.call.ListGroupsCall;
@@ -20,15 +21,10 @@ import java.util.Map;
 
 public class GmsClient {
 
-    HttpClientWrapper httpClientWrapper;
+    private final HttpClientWrapper httpClientWrapper;
 
-    public GmsClient(String baseUrl) {
-
-        if (!baseUrl.endsWith("/")) {
-            baseUrl += "/";
-        }
-
-        httpClientWrapper = new HttpClientWrapper(baseUrl);
+    GmsClient(HttpClientWrapper httpClientWrapper) {
+        this.httpClientWrapper = httpClientWrapper;
     }
 
     public GmsClient setAccessToken(String accessToken) {
@@ -83,4 +79,8 @@ public class GmsClient {
     public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) {
         new AddInvitedRegistrationCall(httpClientWrapper).addInvitedRegistration(token, email, groupsPermissions);
     }
+
+    public List<String> getMemberEmailAddresses(String groupId, Permission permission) {
+        return new GetMemberEmailAddresses(httpClientWrapper).getMemberEmailAddresses(groupId, permission);
+    }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java
new file mode 100644
index 0000000..002081d
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java
@@ -0,0 +1,40 @@
+package it.inaf.ia2.gms.client;
+
+import it.inaf.ia2.gms.client.call.HttpClientWrapper;
+
+public class GmsClientBuilder {
+
+    private String gmsBaseUrl;
+    private String rapBaseUrl;
+    private String clientId;
+    private String clientSecret;
+
+    public GmsClientBuilder setGmsBaseUrl(String gmsBaseUrl) {
+        this.gmsBaseUrl = gmsBaseUrl;
+        return this;
+    }
+
+    public GmsClientBuilder setRapBaseUrl(String rapBaseUrl) {
+        this.rapBaseUrl = rapBaseUrl;
+        return this;
+    }
+
+    public GmsClientBuilder setClientId(String clientId) {
+        this.clientId = clientId;
+        return this;
+    }
+
+    public GmsClientBuilder setClientSecret(String clientSecret) {
+        this.clientSecret = clientSecret;
+        return this;
+    }
+
+    public GmsClient build() {
+        HttpClientWrapper clientWrapper = new HttpClientWrapper(gmsBaseUrl);
+        if (rapBaseUrl != null && clientId != null && clientSecret != null) {
+            clientWrapper.setRapBaseUrl(rapBaseUrl)
+                    .setClientId(clientId).setClientSecret(clientSecret);
+        }
+        return new GmsClient(clientWrapper);
+    }
+}
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
index 6f03160..161de1f 100644
--- 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
@@ -27,13 +27,13 @@ public abstract class BaseGmsCall {
         return clientWrapper.newHttpRequest(endpoint);
     }
 
-    protected void logServerError(HttpRequest request, HttpResponse<String> response) {
+    protected static 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());
+                + "\nServer response text is " + response.body());
     }
 
-    protected void logServerErrorInputStream(HttpRequest request, HttpResponse<InputStream> response) {
+    protected static 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() : "";
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
new file mode 100644
index 0000000..a69913b
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
@@ -0,0 +1,50 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.model.Permission;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+public class GetMemberEmailAddresses extends BaseGmsCall {
+
+    public GetMemberEmailAddresses(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    public List<String> getMemberEmailAddresses(String group, Permission permission) {
+
+        List<String> emailAddresses = new ArrayList<>();
+
+        String endpoint = "email/" + group;
+        if (permission != null) {
+            endpoint += "?permission=" + permission;
+        }
+
+        HttpRequest request = newHttpRequest(endpoint)
+                .header("Accept", "text/plain")
+                .GET()
+                .build();
+
+        return getClient().sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
+                .thenApply(response -> {
+                    if (response.statusCode() == 200) {
+                        return response.body();
+                    }
+                    logServerErrorInputStream(request, 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()) {
+                                emailAddresses.add(line);
+                            }
+                        }
+                    }
+                    return emailAddresses;
+                }).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
index 7bbf781..7d90d72 100644
--- 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
@@ -4,12 +4,20 @@ import java.net.URI;
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse;
+import java.util.Base64;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public class HttpClientWrapper {
 
     private final String baseGmsUri;
     private final HttpClient client;
 
+    private String rapBaseUrl;
+    private String clientId;
+    private String clientSecret;
+
     private String accessToken;
 
     public HttpClientWrapper(String baseGmsUri) {
@@ -29,12 +37,68 @@ public class HttpClientWrapper {
         return this;
     }
 
+    public HttpClientWrapper setRapBaseUrl(String rapBaseUrl) {
+        if (!rapBaseUrl.endsWith("/")) {
+            rapBaseUrl += "/";
+        }
+        this.rapBaseUrl = rapBaseUrl;
+        return this;
+    }
+
+    public HttpClientWrapper setClientId(String clientId) {
+        this.clientId = clientId;
+        return this;
+    }
+
+    public HttpClientWrapper setClientSecret(String clientSecret) {
+        this.clientSecret = clientSecret;
+        return this;
+    }
+
     Builder newHttpRequest(String endpoint) {
+        if (accessToken == null) {
+            accessToken = getAccessTokenFromClientCredentials();
+        }
         return HttpRequest.newBuilder()
                 .uri(URI.create(baseGmsUri + endpoint))
                 .header("Authorization", "Bearer " + accessToken);
     }
 
+    private String getAccessTokenFromClientCredentials() {
+        if (rapBaseUrl == null || clientId == null || clientSecret == null) {
+            throw new IllegalStateException("Access token is null and client credentials are not configured");
+        }
+
+        String basicAuthHeader = clientId + ":" + clientSecret;
+
+        HttpRequest tokenRequest = HttpRequest.newBuilder()
+                .uri(URI.create(rapBaseUrl + "auth/oauth2/token"))
+                .header("Authorization", "Basic " + Base64.getEncoder().encodeToString(basicAuthHeader.getBytes()))
+                .header("Accept", "application/json")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(HttpRequest.BodyPublishers.ofString("grant_type=client_credentials"))
+                .build();
+
+        return client.sendAsync(tokenRequest, HttpResponse.BodyHandlers.ofString())
+                .thenApply(response -> {
+                    if (response.statusCode() == 200) {
+                        return getAccessTokenFromResponse(response.body());
+                    }
+                    BaseGmsCall.logServerError(tokenRequest, response);
+
+                    throw new IllegalStateException("Unable to retrieve access token");
+                }).join();
+    }
+
+    protected String getAccessTokenFromResponse(String body) {
+        Pattern codePattern = Pattern.compile(".*\"access_token\":\\s*\"([^\"]+).*");
+        Matcher matcher = codePattern.matcher(body);
+        if (matcher.find()) {
+            return matcher.group(1);
+        }
+        throw new IllegalStateException("Unable to extract access token from body");
+    }
+
     HttpClient getClient() {
         return client;
     }
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 b086de3..ce84caa 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
@@ -7,7 +7,6 @@ import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublisher;
 import java.net.http.HttpResponse;
 import java.net.http.HttpResponse.BodySubscriber;
 import java.net.http.HttpResponse.BodySubscribers;
@@ -18,7 +17,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Flow;
-import java.util.concurrent.Flow.Subscriber;
 import static org.junit.Assert.assertEquals;
 import org.junit.Before;
 import org.junit.Test;
@@ -47,9 +45,9 @@ public class GmsClientTest {
         httpClient = mock(HttpClient.class);
 
         HttpClientWrapper clientWrapper = new MockedHttpClientWrapper(BASE_URL, httpClient);
+        clientWrapper.setAccessToken("foo");
 
-        client = new GmsClient(BASE_URL);
-        client.httpClientWrapper = clientWrapper;
+        client = new GmsClient(clientWrapper);
     }
 
     @Test
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java
new file mode 100644
index 0000000..4bc0924
--- /dev/null
+++ b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java
@@ -0,0 +1,15 @@
+package it.inaf.ia2.gms.client.call;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+public class HttpClientWrapperTest {
+
+    @Test
+    public void testExtractAccessToken() {
+        String response = "{\"access_token\":\"TEST_TOKEN\",\"token_type\":\"Bearer\",\"expires_in\":3600}";
+
+        HttpClientWrapper clientWrapper = new HttpClientWrapper("http://localhost");
+        assertEquals("TEST_TOKEN", clientWrapper.getAccessTokenFromResponse(response));
+    }
+}
diff --git a/gms/pom.xml b/gms/pom.xml
index c799494..afe66f7 100644
--- a/gms/pom.xml
+++ b/gms/pom.xml
@@ -62,6 +62,7 @@
     </dependencies>
 
     <build>
+        <finalName>gms</finalName>
         <plugins>
             <plugin>
                 <groupId>com.github.eirslett</groupId>
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 a5ff5cf..9677cdf 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
@@ -7,6 +7,7 @@ import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
 import it.inaf.ia2.gms.manager.MembershipManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
 import it.inaf.ia2.gms.model.Permission;
+import it.inaf.ia2.gms.model.RapUser;
 import it.inaf.ia2.gms.model.response.UserPermission;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.PermissionsDAO;
@@ -22,9 +23,11 @@ import java.io.PrintWriter;
 import java.security.Principal;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -301,6 +304,31 @@ public class JWTWebServiceController {
         response.setStatus(HttpServletResponse.SC_CREATED);
     }
 
+    @GetMapping(value = "/email/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
+    public void getEmailOfMembers(@PathVariable("group") String groupNames, @RequestParam("permission") Optional<Permission> permission, HttpServletResponse response) throws IOException {
+
+        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
+
+        Set<String> selectedUserIds = null;
+        if (permission.isPresent()) {
+            Permission desiredPermission = permission.get();
+            selectedUserIds = new HashSet<>();
+            for (PermissionEntity groupsPermission : permissionsDAO.getGroupsPermissions(groupEntity.getId())) {
+                if (Permission.includes(groupsPermission.getPermission(), desiredPermission)) {
+                    selectedUserIds.add(groupsPermission.getUserId());
+                }
+            }
+        }
+
+        try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
+            for (RapUser member : membershipManager.getMembers(groupEntity)) {
+                if (selectedUserIds == null || selectedUserIds.contains(member.getId())) {
+                    pw.println(member.getPrimaryEmail());
+                }
+            }
+        }
+    }
+
     private GroupEntity getGroupFromNames(List<String> groupNames) {
         if (groupNames.isEmpty()) {
             return getRoot();
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java
index 3f2acf9..022e542 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java
@@ -48,7 +48,7 @@ public class MembershipManager extends UserAwareComponent {
 
         Permission groupPermission = permissionsManager.getCurrentUserPermission(group);
 
-        if (groupPermission == Permission.TRAVERSE) {
+        if (!Permission.includes(groupPermission, Permission.VIEW_MEMBERS)) {
             throw new UnauthorizedException("You don't have the permission to view members");
         }
 
@@ -98,8 +98,8 @@ public class MembershipManager extends UserAwareComponent {
 
     private Permission verifyUserCanManageMembers(GroupEntity group) {
         Permission permission = permissionsManager.getCurrentUserPermission(group);
-        if (permission != Permission.ADMIN && permission != Permission.MANAGE_MEMBERS) {
-            throw new UnauthorizedException("Missing admin or manage members permissions");
+        if (!Permission.includes(permission, Permission.MANAGE_MEMBERS)) {
+            throw new UnauthorizedException("Missing manage members permissions");
         }
         return permission;
     }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/Permission.java b/gms/src/main/java/it/inaf/ia2/gms/model/Permission.java
index 166c1dc..8beb45a 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/model/Permission.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/Permission.java
@@ -32,4 +32,25 @@ public enum Permission {
 
         return oldPermission;
     }
+
+    public static boolean includes(Permission permission, Permission permissionToCheck) {
+        if (permissionToCheck == null) {
+            throw new IllegalArgumentException("Permission to check cannot be null");
+        }
+
+        if (permission == null) {
+            return false;
+        }
+        switch (permissionToCheck) {
+            case ADMIN:
+                return permission == ADMIN;
+            case MANAGE_MEMBERS:
+                return permission == MANAGE_MEMBERS || permission == ADMIN;
+            case VIEW_MEMBERS:
+                return permission != TRAVERSE;
+            case TRAVERSE:
+                return true;
+        }
+        return false;
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java b/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java
index 5ef0ac4..846b317 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java
@@ -40,9 +40,7 @@ public class RapUser {
         }
 
         if (displayName == null) { // No name and surname --> using primary email
-            Identity primaryIdentity = identities.stream().filter(i -> i.isPrimary()).findFirst()
-                    .orElseThrow(() -> new IllegalStateException("No primary identity for user " + id));
-            displayName = primaryIdentity.getEmail();
+            displayName = getPrimaryEmail();
         }
 
         // Adding types
@@ -56,4 +54,10 @@ public class RapUser {
 
         return displayName;
     }
+
+    public String getPrimaryEmail() {
+        Identity primaryIdentity = identities.stream().filter(i -> i.isPrimary()).findFirst()
+                .orElseThrow(() -> new IllegalStateException("No primary identity for user " + id));
+        return primaryIdentity.getEmail();
+    }
 }
diff --git a/gms/src/main/resources/application.properties b/gms/src/main/resources/application.properties
index 19b3dac..b096d05 100644
--- a/gms/src/main/resources/application.properties
+++ b/gms/src/main/resources/application.properties
@@ -17,7 +17,7 @@ logging.level.org.springframework.security=DEBUG
 logging.level.org.springframework.jdbc=TRACE
 logging.level.org.springframework.web=TRACE
 
-spring.datasource.url=jdbc:postgresql://localhost:5433/postgres
+spring.datasource.url=jdbc:postgresql://localhost:5432/gms2
 spring.datasource.username=gms
 spring.datasource.password=gms
 
diff --git a/gms/src/test/java/it/inaf/ia2/gms/model/PermissionTest.java b/gms/src/test/java/it/inaf/ia2/gms/model/PermissionTest.java
index 136befa..bf642bd 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/model/PermissionTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/model/PermissionTest.java
@@ -1,6 +1,8 @@
 package it.inaf.ia2.gms.model;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -37,4 +39,32 @@ public class PermissionTest {
         assertEquals(Permission.MANAGE_MEMBERS, Permission.addPermission(Permission.TRAVERSE, Permission.MANAGE_MEMBERS));
         assertEquals(Permission.VIEW_MEMBERS, Permission.addPermission(Permission.TRAVERSE, Permission.VIEW_MEMBERS));
     }
+
+    @Test
+    public void includesTest() {
+
+        assertTrue(Permission.includes(Permission.ADMIN, Permission.ADMIN));
+        assertFalse(Permission.includes(Permission.MANAGE_MEMBERS, Permission.ADMIN));
+        assertFalse(Permission.includes(Permission.VIEW_MEMBERS, Permission.ADMIN));
+        assertFalse(Permission.includes(Permission.TRAVERSE, Permission.ADMIN));
+        assertFalse(Permission.includes(null, Permission.ADMIN));
+
+        assertTrue(Permission.includes(Permission.ADMIN, Permission.MANAGE_MEMBERS));
+        assertTrue(Permission.includes(Permission.MANAGE_MEMBERS, Permission.MANAGE_MEMBERS));
+        assertFalse(Permission.includes(Permission.VIEW_MEMBERS, Permission.MANAGE_MEMBERS));
+        assertFalse(Permission.includes(Permission.TRAVERSE, Permission.MANAGE_MEMBERS));
+        assertFalse(Permission.includes(null, Permission.MANAGE_MEMBERS));
+
+        assertTrue(Permission.includes(Permission.ADMIN, Permission.VIEW_MEMBERS));
+        assertTrue(Permission.includes(Permission.MANAGE_MEMBERS, Permission.VIEW_MEMBERS));
+        assertTrue(Permission.includes(Permission.VIEW_MEMBERS, Permission.VIEW_MEMBERS));
+        assertFalse(Permission.includes(Permission.TRAVERSE, Permission.VIEW_MEMBERS));
+        assertFalse(Permission.includes(null, Permission.ADMIN));
+
+        assertTrue(Permission.includes(Permission.ADMIN, Permission.TRAVERSE));
+        assertTrue(Permission.includes(Permission.MANAGE_MEMBERS, Permission.TRAVERSE));
+        assertTrue(Permission.includes(Permission.VIEW_MEMBERS, Permission.TRAVERSE));
+        assertTrue(Permission.includes(Permission.TRAVERSE, Permission.TRAVERSE));
+        assertFalse(Permission.includes(null, Permission.TRAVERSE));
+    }
 }
-- 
GitLab