Implemented a service to list clients registered by a user (close #52)

Change-Id: I3b828ec8e7a50573b52bfb1fec59f3430f9be785
diff --git a/full/Changes b/full/Changes
index 257ee98..372efbe 100644
--- a/full/Changes
+++ b/full/Changes
@@ -16,6 +16,8 @@
 14/11/2019
    - Added client description and URL to list-authorized-clients service 
      (margaretha, close #53)     
+   - Implemented a service to list clients registered by a user (margaretha, 
+     close #52)   
 
 # version 0.62.1
 08/07/2019
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
index 896d77b..ef44bfa 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
@@ -107,7 +107,7 @@
         client = entityManager.merge(client);
     }
 
-    public List<OAuth2Client> retrieveUserClients (String username)
+    public List<OAuth2Client> retrieveUserAuthorizedClients (String username)
             throws KustvaktException {
         ParameterChecker.checkStringValue(username, "username");
 
@@ -133,4 +133,21 @@
         return q.getResultList();
     }
 
+    public List<OAuth2Client> retrieveUserRegisteredClients (String username)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<OAuth2Client> query =
+                builder.createQuery(OAuth2Client.class);
+
+        Root<OAuth2Client> client = query.from(OAuth2Client.class);
+        query.select(client);
+        query.where(builder.equal(client.get(OAuth2Client_.registeredBy),
+                username));
+        query.distinct(true);
+        TypedQuery<OAuth2Client> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
index 9bf6259..de7a90c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
@@ -349,18 +349,35 @@
         return clientDao.retrieveClientById(clientId);
     }
 
-    public List<OAuth2UserClientDto> listUserClients (String username,
+    public List<OAuth2UserClientDto> listUserAuthorizedClients (String username,
             String clientId, String clientSecret) throws KustvaktException {
         OAuth2Client client = authenticateClient(clientId, clientSecret);
         if (!client.isSuper()) {
             throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
-                    "Only super client is allowed to list user clients.",
+                    "Only super client is allowed to list user authorized clients.",
                     OAuth2Error.UNAUTHORIZED_CLIENT);
         }
         List<OAuth2Client> userClients =
-                clientDao.retrieveUserClients(username);
+                clientDao.retrieveUserAuthorizedClients(username);
         Collections.sort(userClients);
-
+        return createClientDtos(userClients);
+    }
+    
+    public List<OAuth2UserClientDto> listUserRegisteredClients (String username,
+            String clientId, String clientSecret) throws KustvaktException {
+        OAuth2Client client = authenticateClient(clientId, clientSecret);
+        if (!client.isSuper()) {
+            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Only super client is allowed to list user registered clients.",
+                    OAuth2Error.UNAUTHORIZED_CLIENT);
+        }
+        List<OAuth2Client> userClients =
+                clientDao.retrieveUserRegisteredClients(username);
+        Collections.sort(userClients);
+        return createClientDtos(userClients);
+    }
+    
+    private List<OAuth2UserClientDto> createClientDtos (List<OAuth2Client> userClients) {
         List<OAuth2UserClientDto> dtoList = new ArrayList<>(userClients.size());
         for (OAuth2Client uc : userClients) {
             if (uc.isSuper()) continue;
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
index 69cb4e0..be4cb9f 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
@@ -125,7 +125,7 @@
     @DELETE
     @Path("deregister/{client_id}")
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-    public Response deregisterPublicClient (
+    public Response deregisterClient (
             @Context SecurityContext securityContext,
             @PathParam("client_id") String clientId,
             @FormParam("client_secret") String clientSecret) {
@@ -264,7 +264,7 @@
             scopeService.verifyScope(tokenContext,
                     OAuth2Scope.LIST_USER_CLIENT);
 
-            return clientService.listUserClients(username, clientId,
+            return clientService.listUserAuthorizedClients(username, clientId,
                     clientSecret);
         }
         catch (KustvaktException e) {
@@ -272,4 +272,42 @@
         }
     }
 
+    /**
+     * Lists clients registered by the authenticated user, e.g. an R
+     * client. This service is intended for client management. It is
+     * not part of the OAuth2 specification. Only super clients are
+     * allowed to use this service. It requires user and client
+     * authentications.
+     * 
+     * @param context
+     * @param clientId
+     *            the client id of the super client
+     * @param clientSecret
+     *            the client secret of the super client
+     * @return a list of clients registered by a user
+     */
+    @POST
+    @Path("registered")
+    @ResourceFilters({ AuthenticationFilter.class, BlockingFilter.class })
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public List<OAuth2UserClientDto> listUserRegisteredClients (
+            @Context SecurityContext context,
+            @FormParam("client_id") String clientId,
+            @FormParam("client_secret") String clientSecret) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+        String username = tokenContext.getUsername();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.LIST_USER_CLIENT);
+
+            return clientService.listUserRegisteredClients(username, clientId,
+                    clientSecret);
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
index 827684e..dfbcee8 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
@@ -56,6 +56,18 @@
             }
         }
     }
+    
+    private ClientResponse registerClient (String username, 
+            OAuth2ClientJson json) throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        return resource().path(API_VERSION).path("oauth2").path("client")
+                .path("register")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "password"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .entity(json).post(ClientResponse.class);
+    }
 
     private ClientResponse registerConfidentialClient ()
             throws KustvaktException {
@@ -67,13 +79,7 @@
         json.setRedirectURI("https://example.client.com/redirect");
         json.setDescription("This is a confidential test client.");
 
-        return resource().path(API_VERSION).path("oauth2").path("client")
-                .path("register")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
-                .entity(json).post(ClientResponse.class);
+        return registerClient(username, json);
     }
 
     private JsonNode retrieveClientInfo (String clientId, String username)
@@ -156,13 +162,7 @@
         json.setRedirectURI("https://test.public.client.com/redirect");
         json.setDescription("This is a public test client.");
 
-        ClientResponse response = resource().path(API_VERSION).path("oauth2")
-                .path("client").path("register")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
-                .entity(json).post(ClientResponse.class);
+        ClientResponse response = registerClient(username, json);
 
         String entity = response.getEntity(String.class);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
@@ -183,13 +183,7 @@
         json.setType(OAuth2ClientType.PUBLIC);
         json.setDescription("This is a desktop test client.");
 
-        ClientResponse response = resource().path(API_VERSION).path("oauth2")
-                .path("client").path("register")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
-                .entity(json).post(ClientResponse.class);
+        ClientResponse response = registerClient(username, json);
 
         String entity = response.getEntity(String.class);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
@@ -200,7 +194,7 @@
 
         testDeregisterPublicClientMissingUserAuthentication(clientId);
         testDeregisterPublicClientMissingId();
-        testDeregisterPublicClient(clientId);
+        testDeregisterPublicClient(clientId,username);
     }
 
     private void testAccessTokenAfterDeregistration (String clientId,
@@ -219,7 +213,7 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         code = requestAuthorizationCode(clientId, "", null, userAuthHeader);
-        testDeregisterPublicClient(clientId);
+        testDeregisterPublicClient(clientId, username);
 
         response = requestTokenWithAuthorizationCodeAndForm(clientId,
                 clientSecret, code);
@@ -266,7 +260,7 @@
         assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(), response.getStatus());
     }
 
-    private void testDeregisterPublicClient (String clientId)
+    private void testDeregisterPublicClient (String clientId, String username)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
 
@@ -467,7 +461,8 @@
                 node.at("/errors/0/1").asText());
     }
 
-    private void requestUserClientList (String userAuthHeader) throws KustvaktException {
+    private void requestAuthorizedClientList (String userAuthHeader)
+            throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("client_id", superClientId);
         form.add("client_secret", clientSecret);
@@ -517,9 +512,10 @@
                 confidentialClientId, clientSecret, code);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
-        requestUserClientList(userAuthHeader);
-        testListClientWithMultipleRefreshTokens(userAuthHeader);
-
+        requestAuthorizedClientList(userAuthHeader);
+        testListAuthorizedClientWithMultipleRefreshTokens(userAuthHeader);
+        
+        
         testRequestTokenWithRevokedRefreshToken(publicClientId, clientSecret,
                 refreshToken);
 
@@ -531,8 +527,8 @@
                 accessToken, refreshToken);
     }
 
-    private void testListClientWithMultipleRefreshTokens (String userAuthHeader)
-            throws KustvaktException {
+    private void testListAuthorizedClientWithMultipleRefreshTokens (
+            String userAuthHeader) throws KustvaktException {
         // client 1
         String code = requestAuthorizationCode(publicClientId, clientSecret,
                 null, userAuthHeader);
@@ -541,7 +537,7 @@
 
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
-        requestUserClientList(userAuthHeader);
+        requestAuthorizedClientList(userAuthHeader);
 
         JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
         String accessToken = node.at("/access_token").asText();
@@ -582,4 +578,38 @@
         testRequestTokenWithRevokedRefreshToken(clientId, clientSecret,
                 refreshToken);
     }
+    
+    @Test
+    public void testListRegisteredClients ()
+            throws KustvaktException {
+        
+        String clientName = "OAuth2DoryClient";
+        OAuth2ClientJson json = new OAuth2ClientJson();
+        json.setName(clientName);
+        json.setType(OAuth2ClientType.PUBLIC);
+        json.setDescription("This is dory client.");
+
+        registerClient("dory", json);
+        
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("client_id", superClientId);
+        form.add("client_secret", clientSecret);
+
+        ClientResponse response = resource().path(API_VERSION).path("oauth2")
+                .path("client").path("registered")
+                .header(Attributes.AUTHORIZATION, userAuthHeader)
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        
+        assertEquals(1, node.size());
+        assertEquals(clientName, node.at("/0/clientName").asText());
+        String clientId = node.at("/0/clientId").asText();
+        testDeregisterPublicClient(clientId, "dory");
+    }
 }