Implemented OAuth2 client registration.

Change-Id: I0c51bb1ee031e2a6ff9d0181de7dcb1da53d1d07
diff --git a/full/src/main/java/de/ids_mannheim/korap/constant/ClientType.java b/full/src/main/java/de/ids_mannheim/korap/constant/ClientType.java
new file mode 100644
index 0000000..eb78b40
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/constant/ClientType.java
@@ -0,0 +1,19 @@
+package de.ids_mannheim.korap.constant;
+
+public enum ClientType {
+
+    // EM: from RFC 6749
+        
+//    Clients capable of maintaining the confidentiality of their
+//    credentials (e.g., client implemented on a secure server with
+//    restricted access to the client credentials), or capable of secure
+//    client authentication using other means.
+    CONFIDENTIAL, 
+    
+//    Clients incapable of maintaining the confidentiality of their
+//    credentials (e.g., clients executing on the device used by the
+//    resource owner, such as an installed native application or a web
+//    browser-based application), and incapable of secure client
+//    authentication via any other means.
+    PUBLIC;
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
new file mode 100644
index 0000000..09d801a
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
@@ -0,0 +1,43 @@
+package de.ids_mannheim.korap.dao;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.persistence.criteria.CriteriaBuilder;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.ClientType;
+import de.ids_mannheim.korap.entity.OAuth2Client;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.ParameterChecker;
+
+@Transactional
+@Repository
+public class OAuth2ClientDao {
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    public void registerClient (String id, String secret, String name,
+            ClientType type, String url, String redirectURI)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(id, "client id");
+        ParameterChecker.checkStringValue(name, "client name");
+        ParameterChecker.checkObjectValue(type, "client type");
+        ParameterChecker.checkStringValue(url, "client url");
+        ParameterChecker.checkStringValue(redirectURI, "client redirect uri");
+
+        OAuth2Client client = new OAuth2Client();
+        client.setId(id);
+        client.setName(name);
+        client.setSecret(secret);
+        client.setType(type);
+        client.setUrl(url);
+        client.setRedirectURI(redirectURI);
+
+        entityManager.persist(client);
+    }
+
+
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
index 1d48b02..74d5b1a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
@@ -1,6 +1,5 @@
 package de.ids_mannheim.korap.dao;
 
-import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
diff --git a/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java b/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
new file mode 100644
index 0000000..c299a22
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
@@ -0,0 +1,39 @@
+package de.ids_mannheim.korap.entity;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+import de.ids_mannheim.korap.constant.ClientType;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "oauth2_client")
+public class OAuth2Client {
+
+    @Id
+    private String id;
+    private String secret;
+    @Enumerated(EnumType.STRING)
+    private ClientType type;
+    @Column(name = "redirect_uri")
+    private String redirectURI;
+    private String url;
+    private String name;
+
+    @Override
+    public String toString () {
+        return "id=" + id + ", secret=" + secret + ", type=" + type + ", name="
+                + name + ", url=" + url + ", redirectURI=" + redirectURI;
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
new file mode 100644
index 0000000..dbfdc2e
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
@@ -0,0 +1,48 @@
+package de.ids_mannheim.korap.service;
+
+import org.apache.commons.validator.routines.UrlValidator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import de.ids_mannheim.korap.constant.ClientType;
+import de.ids_mannheim.korap.dao.OAuth2ClientDao;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
+
+@Service
+public class OAuth2ClientService {
+
+    @Autowired
+    private OAuth2ClientDao clientDao;
+    @Autowired
+    private UrlValidator urlValidator;
+    @Autowired
+    private EncryptionIface encryption;
+
+
+    public void registerClient (OAuth2ClientJson clientJson)
+            throws KustvaktException {
+        if (!urlValidator.isValid(clientJson.getUrl())) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    clientJson.getUrl() + " is invalid.", clientJson.getUrl());
+        }
+        if (!urlValidator.isValid(clientJson.getRedirectURI())) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    clientJson.getRedirectURI() + " is invalid.",
+                    clientJson.getRedirectURI());
+        }
+
+        String secret = null;
+        if (clientJson.getType().equals(ClientType.CONFIDENTIAL)) {
+            secret = encryption.createToken();
+        }
+
+        String id = encryption.createRandomNumber();
+        
+        clientDao.registerClient(id, secret, clientJson.getName(),
+                clientJson.getType(), clientJson.getUrl(),
+                clientJson.getRedirectURI());
+    }
+}
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
new file mode 100644
index 0000000..b0b88e4
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
@@ -0,0 +1,59 @@
+package de.ids_mannheim.korap.web.controller;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import com.sun.jersey.spi.container.ResourceFilters;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.service.OAuth2ClientService;
+import de.ids_mannheim.korap.web.FullResponseHandler;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.BlockingFilter;
+import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
+
+
+@Controller
+@Path("/client")
+//@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+//@ResourceFilters({ AuthenticationFilter.class, BlockingFilter.class })
+public class OAuthClientController {
+
+    @Autowired
+    private OAuth2ClientService clientService;
+    @Autowired
+    private FullResponseHandler responseHandler;
+
+    /** EM: who can register a client?
+     * 
+     * The authorization server SHOULD document the size of any identifier 
+     * it issues.
+     * 
+     * @param context
+     * @param clientJson
+     * @return
+     */
+    @POST
+    @Path("register")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response registerClient (@Context SecurityContext context,
+            OAuth2ClientJson clientJson) {
+        try {
+            clientService.registerClient(clientJson);
+        }
+        catch (KustvaktException e) {
+            responseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    }
+
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthController.java
index b43893b..fdc2046 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthController.java
@@ -54,7 +54,6 @@
 import de.ids_mannheim.korap.handlers.OAuth2Handler;
 import de.ids_mannheim.korap.interfaces.AuthenticationManagerIface;
 import de.ids_mannheim.korap.interfaces.EncryptionIface;
-import de.ids_mannheim.korap.server.KustvaktServer;
 import de.ids_mannheim.korap.user.TokenContext;
 import de.ids_mannheim.korap.user.User;
 import de.ids_mannheim.korap.user.UserDetails;
@@ -74,7 +73,7 @@
  */
 //todo: only allow oauth2 access_token requests GET methods?
 //todo: allow refresh tokens
-@Path(KustvaktServer.API_VERSION + "/oauth2")
+@Path("/oauth2")
 public class OAuthController {
 
     @Autowired
@@ -83,17 +82,18 @@
     private OAuth2Handler handler;
     @Autowired
     private AuthenticationManagerIface controller;
-    private EncryptionIface crypto;
+    
+//    private EncryptionIface crypto;
+    @Autowired
     private KustvaktConfiguration config;
 
 
     public OAuthController () {
-        this.handler = new OAuth2Handler(BeansFactory.getKustvaktContext()
-                .getPersistenceClient());
+//        this.handler = new OAuth2Handler(BeansFactory.getKustvaktContext()
+//                .getPersistenceClient());
 //        this.controller = BeansFactory.getKustvaktContext()
 //                .getAuthenticationManager();
-        this.crypto = BeansFactory.getKustvaktContext().getEncryption();
-        this.config = BeansFactory.getKustvaktContext().getConfiguration();
+//        this.crypto = BeansFactory.getKustvaktContext().getEncryption();
     }
 
 
@@ -118,31 +118,31 @@
     }
 
 
-    @POST
-    @Path("register")
-    @ResourceFilters({ AuthenticationFilter.class, BlockingFilter.class })
-    public Response registerClient (@Context SecurityContext context,
-            @HeaderParam("Host") String host,
-            @QueryParam("redirect_url") String rurl) {
-        ClientInfo info = new ClientInfo(crypto.createRandomNumber(),
-                crypto.createToken());
-        info.setUrl(host);
-        if (rurl == null)
-            throw kustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT,
-                    "Missing parameter!", "redirect_url");
-        info.setRedirect_uri(rurl);
-        TokenContext ctx = (TokenContext) context.getUserPrincipal();
-        String json = "";
-        try {
-            User user = this.controller.getUser(ctx.getUsername());
-            this.handler.getPersistenceHandler().registerClient(info, user);
-            json = info.toJSON();
-        }
-        catch (KustvaktException e) {
-            throw kustvaktResponseHandler.throwit(e);
-        }
-        return Response.ok(json).build();
-    }
+//    @POST
+//    @Path("register")
+//    @ResourceFilters({ AuthenticationFilter.class, BlockingFilter.class })
+//    public Response registerClient (@Context SecurityContext context,
+//            @HeaderParam("Host") String host,
+//            @QueryParam("redirect_url") String rurl) {
+//        ClientInfo info = new ClientInfo(crypto.createRandomNumber(),
+//                crypto.createToken());
+//        info.setUrl(host);
+//        if (rurl == null)
+//            throw kustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT,
+//                    "Missing parameter!", "redirect_url");
+//        info.setRedirect_uri(rurl);
+//        TokenContext ctx = (TokenContext) context.getUserPrincipal();
+//        String json = "";
+//        try {
+//            User user = this.controller.getUser(ctx.getUsername());
+//            this.handler.getPersistenceHandler().registerClient(info, user);
+//            json = info.toJSON();
+//        }
+//        catch (KustvaktException e) {
+//            throw kustvaktResponseHandler.throwit(e);
+//        }
+//        return Response.ok(json).build();
+//    }
 
 
     @GET
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
index ecc00bb..e96ec0a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
@@ -215,6 +215,8 @@
     }
 
     /** Invites group members to join a user-group specified in the JSON object.
+     * Only user-group admins and system admins are allowed. 
+     * 
      * @param securityContext
      * @param group UserGroupJson containing groupId and usernames to be invited
      * as members 
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java b/full/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java
new file mode 100644
index 0000000..28a2a6a
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java
@@ -0,0 +1,16 @@
+package de.ids_mannheim.korap.web.input;
+
+import de.ids_mannheim.korap.constant.ClientType;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+public class OAuth2ClientJson {
+    
+    // all required for registration
+    private String name;
+    private ClientType type;
+    private String url;
+    private String redirectURI;
+}
diff --git a/full/src/main/resources/db/new-mysql/V1.1__create_virtual_corpus_tables.sql b/full/src/main/resources/db/new-mysql/V1.1__create_virtual_corpus_tables.sql
index 1fe0f26..3b8022b 100644
--- a/full/src/main/resources/db/new-mysql/V1.1__create_virtual_corpus_tables.sql
+++ b/full/src/main/resources/db/new-mysql/V1.1__create_virtual_corpus_tables.sql
@@ -61,7 +61,7 @@
   created_by varchar(100) NOT NULL,
   description varchar(255) DEFAULT NULL,
   status varchar(100) DEFAULT NULL,
-  corpus_query varchar(2000) NOT NULL,
+  corpus_query TEXT NOT NULL,
   definition varchar(255) DEFAULT NULL,
   INDEX owner_index (created_by),
   INDEX type_index (type)
diff --git a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
new file mode 100644
index 0000000..e36b0f9
--- /dev/null
+++ b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
@@ -0,0 +1,50 @@
+-- EM: modified from Michael Hanl version
+
+-- oauth2 db tables
+create table if not exists oauth2_client (
+	id VARCHAR(100) UNIQUE PRIMARY KEY,
+	secret VARCHAR(200),
+	type VARCHAR(200) NOT NULL,
+	redirect_uri TEXT NOT NULL,
+--is_confidential BOOLEAN DEFAULT FALSE,
+	url TEXT UNIQUE NOT NULL,
+	name VARCHAR(200) NOT NULL
+);
+
+
+-- status 1 = valid, 0 = revoked, -1 = disabled
+create table if not exists oauth2_access_token (
+id INTEGER PRIMARY KEY AUTO_INCREMENT,
+access_token VARCHAR(300),
+auth_code VARCHAR(250),
+client_id VARCHAR(100),
+user_id INTEGER,
+-- make boolean --
+status INTEGER DEFAULT 1,
+-- in case of code authorization, should match auth code scopes!
+-- use scopes for levelaccess descriptor level[rw],level[r]
+scopes VARCHAR(350),
+expiration TIMESTAMP,
+FOREIGN KEY (user_id)
+REFERENCES korap_users(id)
+ON DELETE CASCADE,
+FOREIGN KEY (client_id)
+REFERENCES oauth2_client(client_id)
+ON DELETE CASCADE
+);
+
+
+-- also scopes?
+create table if not exists oauth2_refresh_token (
+id INTEGER PRIMARY KEY AUTO_INCREMENT,
+client_id VARCHAR(100),
+user_id INTEGER,
+expiration TIMESTAMP,
+scopes VARCHAR(350),
+FOREIGN KEY (user_id)
+REFERENCES korap_users(id)
+ON DELETE CASCADE,
+FOREIGN KEY (client_id)
+REFERENCES oauth2_client(client_id)
+ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/full/src/main/resources/db/new-sqlite/V1.1__create_virtual_corpus_tables.sql b/full/src/main/resources/db/new-sqlite/V1.1__create_virtual_corpus_tables.sql
index d1bbcb7..d2702f7 100644
--- a/full/src/main/resources/db/new-sqlite/V1.1__create_virtual_corpus_tables.sql
+++ b/full/src/main/resources/db/new-sqlite/V1.1__create_virtual_corpus_tables.sql
@@ -71,7 +71,7 @@
   created_by varchar(100) NOT NULL,
   description varchar(255) DEFAULT NULL,
   status varchar(100) DEFAULT NULL,
-  corpus_query varchar(2000) NOT NULL,
+  corpus_query TEXT NOT NULL,
   definition varchar(255) DEFAULT NULL
 );
 
diff --git a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
new file mode 100644
index 0000000..ecf7a24
--- /dev/null
+++ b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
@@ -0,0 +1,11 @@
+-- EM: modified from Michael Hanl version
+
+-- oauth2 db tables
+create table IF NOT EXISTS oauth2_client (
+	id VARCHAR(100) NOT NULL,
+	secret VARCHAR(200) NOT NULL,
+	type VARCHAR(200) NOT NULL,
+	redirect_uri TEXT NOT NULL,
+	url TEXT UNIQUE NOT NULL,
+	name VARCHAR(200) NOT NULL
+);
diff --git a/full/src/main/resources/default-config.xml b/full/src/main/resources/default-config.xml
index e901b2a..2844538 100644
--- a/full/src/main/resources/default-config.xml
+++ b/full/src/main/resources/default-config.xml
@@ -169,7 +169,12 @@
 		<constructor-arg value="${krill.indexDir}"/>
 	</bean>
 
-
+	<!-- URLValidator -->
+	<bean id="urlValidator" class="org.apache.commons.validator.routines.UrlValidator">
+		<constructor-arg value="http,https"/>
+	</bean>
+	
+	
 	<bean id="kustvakt_rewrite" class="de.ids_mannheim.korap.rewrite.FullRewriteHandler">
 		<constructor-arg ref="kustvakt_config" />
 	</bean>
diff --git a/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java b/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
index 930e454..9173038 100644
--- a/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
@@ -20,7 +20,7 @@
 public abstract class SpringJerseyTest extends JerseyTest {
 
     private static String[] classPackages =
-            new String[] { "de.ids_mannheim.korap.web.service.full",
+            new String[] { "de.ids_mannheim.korap.web.controller",
                     "de.ids_mannheim.korap.web.filter",
                     "de.ids_mannheim.korap.web.utils" };
 
diff --git a/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java b/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
index 11f326f..40d4abd 100644
--- a/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
@@ -2,15 +2,11 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
-import javax.validation.constraints.AssertTrue;
-
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
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
new file mode 100644
index 0000000..26a11bc
--- /dev/null
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
@@ -0,0 +1,34 @@
+package de.ids_mannheim.korap.web.controller;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.http.entity.ContentType;
+import org.junit.Test;
+
+import com.google.common.net.HttpHeaders;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.ClientResponse.Status;
+
+import de.ids_mannheim.korap.config.SpringJerseyTest;
+import de.ids_mannheim.korap.constant.ClientType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
+
+public class OAuth2ClientControllerTest extends SpringJerseyTest {
+
+    @Test
+    public void testRegisterClient () throws KustvaktException {
+
+        OAuth2ClientJson json = new OAuth2ClientJson();
+        json.setName("OAuth2ClientControllerTest");
+        json.setType(ClientType.CONFIDENTIAL);
+        json.setUrl("http://example.client.com");
+        json.setRedirectURI("http://example.client.com/redirect");
+
+        ClientResponse response = resource().path("client").path("register")
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .entity(json).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+}
diff --git a/full/src/test/resources/test-config.xml b/full/src/test/resources/test-config.xml
index 83291a9..ffd8c9e 100644
--- a/full/src/test/resources/test-config.xml
+++ b/full/src/test/resources/test-config.xml
@@ -165,6 +165,10 @@
 		<constructor-arg value="${krill.indexDir}" />
 	</bean>
 
+	<!-- URLValidator -->
+	<bean id="urlValidator" class="org.apache.commons.validator.routines.UrlValidator">
+		<constructor-arg value="http,https"/>
+	</bean>
 
 	<bean id="kustvakt_rewrite" class="de.ids_mannheim.korap.rewrite.FullRewriteHandler">
 		<constructor-arg ref="kustvakt_config" />