From 8ed39156072655c3aeba3e880b18d8c7e5c39254 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Mon, 22 Dec 2014 12:15:42 +0100
Subject: [PATCH] OO-1349: implements guards (ip and safe exam browser), update
 and setup SQL scripts

---
 pom.xml                                       |   2 +
 .../fullWebApp/BaseFullWebappController.java  |   2 +-
 src/main/java/org/olat/core/util/Encoder.java |  60 +++++----
 src/main/java/org/olat/core/util/IPUtils.java | 104 +++++++++++++++
 .../org/olat/core/util/event/EventAgency.java |  52 ++++----
 .../course/assessment/AssessmentMode.java     |   6 +
 .../assessment/AssessmentModeManager.java     |  22 ++++
 .../manager/AssessmentModeManagerImpl.java    |  52 ++++++++
 .../assessment/manager/IPRangeChecker.java    |  63 ----------
 .../assessment/model/AssessmentModeImpl.java  |  26 +++-
 .../model/TransientAssessmentMode.java        |  54 ++++++++
 .../ui/AssessmentModeEditController.java      |  46 ++++++-
 ...essmentModeUserConfirmationController.java | 119 ++++++++++++++++--
 .../ui/ChooseStartElementController.java      | 101 +++++++++++++++
 .../assessment/ui/_content/choose_mode.html   |  16 ++-
 .../ui/_content/course_element.html           |   4 +
 .../ui/_i18n/LocalStrings_de.properties       |   8 +-
 .../database/mysql/alter_10_1_0_to_10_2_0.sql |   2 +
 .../database/mysql/setupDatabase.sql          |  50 ++++++++
 .../oracle/alter_10_1_0_to_10_2_0.sql         |  51 ++++++++
 .../database/oracle/setupDatabase.sql         |  54 +++++++-
 .../postgresql/alter_10_1_0_to_10_2_0.sql     |  51 ++++++++
 .../database/postgresql/setupDatabase.sql     |  52 ++++++++
 .../java/org/olat/core/util/IPUtilsTest.java  |  41 ++++++
 .../manager/AssessmentModeManagerTest.java    |  35 ++++++
 .../java/org/olat/test/AllTestsJunit4.java    |   1 +
 26 files changed, 937 insertions(+), 137 deletions(-)
 create mode 100644 src/main/java/org/olat/core/util/IPUtils.java
 delete mode 100644 src/main/java/org/olat/course/assessment/manager/IPRangeChecker.java
 create mode 100644 src/main/java/org/olat/course/assessment/ui/ChooseStartElementController.java
 create mode 100644 src/main/java/org/olat/course/assessment/ui/_content/course_element.html
 create mode 100644 src/main/resources/database/oracle/alter_10_1_0_to_10_2_0.sql
 create mode 100644 src/main/resources/database/postgresql/alter_10_1_0_to_10_2_0.sql
 create mode 100644 src/test/java/org/olat/core/util/IPUtilsTest.java

diff --git a/pom.xml b/pom.xml
index 1010e476179..6e1bf36c5ee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1649,6 +1649,7 @@
 			<version>1.4</version>
 		</dependency>
 		<dependency>
+			<!-- Used by at least velocity engine -->
 			<groupId>commons-collections</groupId>
 			<artifactId>commons-collections</artifactId>
 			<version>3.2.1</version>
@@ -1933,6 +1934,7 @@
 			<version>4.3.3</version>
 		</dependency>
 		<dependency>
+			<!-- Used by at least ical4j, velocity, basiclti -->
 			<groupId>commons-lang</groupId>
 			<artifactId>commons-lang</artifactId>
 			<version>2.6</version>
diff --git a/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java b/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java
index eaa273f4eec..10e99394f95 100644
--- a/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java
+++ b/src/main/java/org/olat/core/commons/fullWebApp/BaseFullWebappController.java
@@ -1195,7 +1195,7 @@ public class BaseFullWebappController extends BasicController implements ChiefCo
 			updateStickyMessage();
 		} else if (AssessmentModeNotificationEvent.command.equals(event.getCommand())
 				&& event instanceof AssessmentModeNotificationEvent) {
-			if(lockResource == null) {
+			if(lockResource == null && getIdentity() != null) {
 				AssessmentModeNotificationEvent amne = (AssessmentModeNotificationEvent)event;
 				if(amne.getAssessedIdentityKeys().contains(getIdentity().getKey())) {
 					asyncLockResource(amne.getAssessementMode());
diff --git a/src/main/java/org/olat/core/util/Encoder.java b/src/main/java/org/olat/core/util/Encoder.java
index 36b5ed51664..59e88a10a7c 100644
--- a/src/main/java/org/olat/core/util/Encoder.java
+++ b/src/main/java/org/olat/core/util/Encoder.java
@@ -57,7 +57,8 @@ public class Encoder {
 		sha1("SHA-1", 100, true),
 		sha256("SHA-256", 100, true),
 		sha512("SHA-512", 100, true),
-		pbkdf2("PBKDF2WithHmacSHA1", 20000, true);
+		pbkdf2("PBKDF2WithHmacSHA1", 20000, true),
+		sha256Exam("SHA-256", 1, false);
 
 		private final boolean salted;
 		private final int iterations;
@@ -108,6 +109,23 @@ public class Encoder {
 		return md5(s, null);
 	}
 	
+	public static String sha256Exam(String s) {
+		try {
+			String HEXES = "0123456789abcdef";
+			
+            java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
+            byte[] array = md.digest(s.getBytes("UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < array.length; ++i) {
+                sb.append(HEXES.charAt((array[i] & 0xF0) >> 4)).append(HEXES.charAt((array[i]  & 0x0F)));
+            }
+            return sb.toString();
+		} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
+			log.error("", e);
+			return null;
+    	}
+	}
+	
 	public static String encrypt(String s, String salt, Algorithm algorithm) {
 		switch(algorithm) {
 			case md5: return md5(s, salt);
@@ -149,10 +167,7 @@ public class Encoder {
 				input = digest.digest(input);
 			}
 			return byteToBase64(input);
-		} catch (NoSuchAlgorithmException e) {
-			log.error("", e);
-			return null;
-		} catch (UnsupportedEncodingException e) {
+		} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
 			log.error("", e);
 			return null;
 		}
@@ -163,10 +178,7 @@ public class Encoder {
 			KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), algorithm.getIterations(), 160);
 			SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm.getAlgorithm());
 			return byteToBase64(f.generateSecret(spec).getEncoded());
-		} catch (NoSuchAlgorithmException e) {
-			log.error("", e);
-			return null;
-		} catch (InvalidKeySpecException e) {
+		} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
 			log.error("", e);
 			return null;
 		}
@@ -174,21 +186,21 @@ public class Encoder {
 	
 	public static String getSalt() {
 	    try {
-				//Always use a SecureRandom generator
-				SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
-				//Create array for salt
-				byte[] salt = new byte[16];
-				//Get a random salt
-				sr.nextBytes(salt);
-				//return salt
-				return byteToBase64(salt);
-			} catch (NoSuchAlgorithmException e) {
-				log.error("", e);
-				return null;
-			}
+			//Always use a SecureRandom generator
+			SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
+			//Create array for salt
+			byte[] salt = new byte[16];
+			//Get a random salt
+			sr.nextBytes(salt);
+			//return salt
+			return byteToBase64(salt);
+		} catch (NoSuchAlgorithmException e) {
+			log.error("", e);
+			return null;
+		}
 	}
 
-  public static String byteToBase64(byte[] data){
-  	return StringHelper.encodeBase64(data);
-  }
+	public static String byteToBase64(byte[] data){
+		return StringHelper.encodeBase64(data);
+	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/core/util/IPUtils.java b/src/main/java/org/olat/core/util/IPUtils.java
new file mode 100644
index 00000000000..bc916fb7a7b
--- /dev/null
+++ b/src/main/java/org/olat/core/util/IPUtils.java
@@ -0,0 +1,104 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.util;
+
+import org.apache.commons.net.util.SubnetUtils;
+
+
+/**
+ * 
+ * Thanks: https://gist.github.com/madan712/6651967
+ * 
+ * It's based of the InetAddresses clas from guava too and
+ * prevent a DNS lookup of java.net.InetAddress
+ * 
+ * Initial date: 18.12.2014<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class IPUtils {
+	
+	public static long ipToLong(byte[] octets) {
+		long result = 0;
+		for (byte octet : octets) {
+			result <<= 8;
+			result |= octet & 0xff;
+		}
+		return result;
+	}
+	
+	public static boolean isValidRange(String ipWithMask, String ipToCheck) {
+		boolean allOk = false;
+		if(ipWithMask.indexOf("/") > 0) {
+			SubnetUtils utils = new SubnetUtils(ipWithMask);
+			allOk = utils.getInfo().isInRange(ipToCheck);	
+		}
+		return allOk;
+	}
+ 
+	/**
+	 * 
+	 * @param ipStart
+	 * @param ipEnd
+	 * @param ipToCheck
+	 * @return
+	 */
+	public static boolean isValidRange(String ipStart, String ipEnd, String ipToCheck) {
+		try {
+			long ipLo = ipToLong(textToNumericFormatV4(ipStart));
+			long ipHi = ipToLong(textToNumericFormatV4(ipEnd));
+			long ipToTest = ipToLong(textToNumericFormatV4(ipToCheck));
+			return (ipToTest >= ipLo && ipToTest <= ipHi);
+		} catch (Exception e) {
+			e.printStackTrace();
+			return false;
+		}
+	}
+
+	private static byte[] textToNumericFormatV4(String ipString) {
+		String[] address = ipString.split("\\.", 5);
+		if (address.length != 4) {
+			return null;
+		}
+
+		byte[] bytes = new byte[4];
+		try {
+			for (int i = 0; i < bytes.length; i++) {
+				bytes[i] = parseOctet(address[i]);
+			}
+		} catch (NumberFormatException ex) {
+			return null;
+		}
+
+		return bytes;
+	}
+
+	private static byte parseOctet(String ipPart) {
+		// Note: we already verified that this string contains only hex digits.
+		int octet = Integer.parseInt(ipPart);
+		// Disallow leading zeroes, because no clear standard exists on
+		// whether these should be interpreted as decimal or octal.
+		if (octet > 255 || (ipPart.startsWith("0") && ipPart.length() > 1)) {
+			throw new NumberFormatException();
+		}
+		return (byte) octet;
+	}
+}
+
diff --git a/src/main/java/org/olat/core/util/event/EventAgency.java b/src/main/java/org/olat/core/util/event/EventAgency.java
index e69f7e5e044..6019d8abd78 100644
--- a/src/main/java/org/olat/core/util/event/EventAgency.java
+++ b/src/main/java/org/olat/core/util/event/EventAgency.java
@@ -89,43 +89,35 @@ class EventAgency {
 		// -> avoid dead lock (see OLAT-3681)
 		//no sync during firing to listeners (potentially "long" taking - although recommendation is to keep event methods short.
 		
-			for (int i = 0; i < liArr.length; i++) {
-				try {
-					final GenericEventListener listener = liArr[i];
-					
-					//make sure GenericEvents are only sent when controller is not yet disposed
-					if (listener instanceof Controller) {
-						Controller dCtrl = (Controller)listener;
-						if (!dCtrl.isDisposed()) {
-							ThreadLocalUserActivityLoggerInstaller.runWithUserActivityLogger(new Runnable() {
-								public void run() {
-									listener.event(event);
-								}
-							}, UserActivityLoggerImpl.newLoggerForEventBus(dCtrl));
-						}
-					} else {
-						if(log.isDebug()){
-							log.debug("fireEvent: Non-Controller: "+listener);
-						}
-						//is there a need to differ the events sent on one VM and in cluster mode?
+		for (int i = 0; i < liArr.length; i++) {
+			try {
+				final GenericEventListener listener = liArr[i];
+				
+				//make sure GenericEvents are only sent when controller is not yet disposed
+				if (listener instanceof Controller) {
+					Controller dCtrl = (Controller)listener;
+					if (!dCtrl.isDisposed()) {
 						ThreadLocalUserActivityLoggerInstaller.runWithUserActivityLogger(new Runnable() {
 							public void run() {
 								listener.event(event);
 							}
-						}, ThreadLocalUserActivityLoggerInstaller.createEmptyUserActivityLogger());
+						}, UserActivityLoggerImpl.newLoggerForEventBus(dCtrl));
 					}
-				} catch (RuntimeException e) {
-					log.error("Error while sending generic event! Removing listener: "+liArr[i], e);
-					//don't remove it!!! removeListener(liArr[i]);
+				} else {
+					if(log.isDebug()){
+						log.debug("fireEvent: Non-Controller: "+listener);
+					}
+					//is there a need to differ the events sent on one VM and in cluster mode?
+					ThreadLocalUserActivityLoggerInstaller.runWithUserActivityLogger(new Runnable() {
+						public void run() {
+							listener.event(event);
+						}
+					}, ThreadLocalUserActivityLoggerInstaller.createEmptyUserActivityLogger());
 				}
+			} catch (RuntimeException e) {
+				log.error("Error while sending generic event: "+liArr[i], e);
 			}
-
-		// remember the latest event for a while so e.g. controller which link to resources res (link called res') can catch up on missed updates.
-		/*if (ttl != 0) {
-			latestEvent = event;
-			latestEventTimestamp = System.currentTimeMillis();
-		}*/
-		
+		}
 	}
 
 	/**
diff --git a/src/main/java/org/olat/course/assessment/AssessmentMode.java b/src/main/java/org/olat/course/assessment/AssessmentMode.java
index a9bc07fcf59..3f986ce8d1a 100644
--- a/src/main/java/org/olat/course/assessment/AssessmentMode.java
+++ b/src/main/java/org/olat/course/assessment/AssessmentMode.java
@@ -49,6 +49,8 @@ public interface AssessmentMode extends ModifiedInfo, CreateInfo {
 	public Date getBegin();
 
 	public void setBegin(Date begin);
+	
+	public Date getBeginWithLeadTime();
 
 	public Date getEnd();
 
@@ -73,6 +75,10 @@ public interface AssessmentMode extends ModifiedInfo, CreateInfo {
 	public String getElementList();
 
 	public void setElementList(String elementList);
+	
+	public String getStartElement();
+
+	public void setStartElement(String startElement);
 
 	public boolean isRestrictAccessIps();
 
diff --git a/src/main/java/org/olat/course/assessment/AssessmentModeManager.java b/src/main/java/org/olat/course/assessment/AssessmentModeManager.java
index 2074dac9618..4a3226ff724 100644
--- a/src/main/java/org/olat/course/assessment/AssessmentModeManager.java
+++ b/src/main/java/org/olat/course/assessment/AssessmentModeManager.java
@@ -22,6 +22,8 @@ package org.olat.course.assessment;
 import java.util.List;
 import java.util.Set;
 
+import javax.servlet.http.HttpServletRequest;
+
 import org.olat.basesecurity.IdentityRef;
 import org.olat.group.BusinessGroup;
 import org.olat.group.area.BGArea;
@@ -61,9 +63,29 @@ public interface AssessmentModeManager {
 	 */
 	public List<AssessmentMode> getAssessmentModeFor(IdentityRef identity);
 	
+	/**
+	 * This return all modes between the begin date minus lead time and end time.
+	 * 
+	 * @return The list of modes
+	 */
 	public List<AssessmentMode> getCurrentAssessmentModes();
 	
 	public Set<Long> getAssessedIdentityKeys(AssessmentMode assessmentMode);
 	
+	/**
+	 * 
+	 * @param ipList
+	 * @param address
+	 * @return
+	 */
+	public boolean isIpAllowed(String ipList, String address);
+	
+	/**
+	 * 
+	 * @param request
+	 * @param safeExamBrowserKey
+	 * @return
+	 */
+	public boolean isSafelyAllowed(HttpServletRequest request, String safeExamBrowserKey);
 
 }
diff --git a/src/main/java/org/olat/course/assessment/manager/AssessmentModeManagerImpl.java b/src/main/java/org/olat/course/assessment/manager/AssessmentModeManagerImpl.java
index 793914f5abe..eab2cc2b36d 100644
--- a/src/main/java/org/olat/course/assessment/manager/AssessmentModeManagerImpl.java
+++ b/src/main/java/org/olat/course/assessment/manager/AssessmentModeManagerImpl.java
@@ -26,10 +26,16 @@ import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.StringTokenizer;
+
+import javax.servlet.http.HttpServletRequest;
 
 import org.olat.basesecurity.GroupRoles;
 import org.olat.basesecurity.IdentityRef;
 import org.olat.core.commons.persistence.DB;
+import org.olat.core.util.Encoder;
+import org.olat.core.util.IPUtils;
+import org.olat.core.util.StringHelper;
 import org.olat.course.assessment.AssessmentMode;
 import org.olat.course.assessment.AssessmentMode.Target;
 import org.olat.course.assessment.AssessmentModeManager;
@@ -82,6 +88,18 @@ public class AssessmentModeManagerImpl implements AssessmentModeManager {
 	public AssessmentMode save(AssessmentMode assessmentMode) {
 		AssessmentMode reloadedMode;
 		assessmentMode.setLastModified(new Date());
+		
+		//update begin with lead time
+		Date begin = assessmentMode.getBegin();
+		Calendar cal = Calendar.getInstance();
+		cal.setTime(begin);
+		if(assessmentMode.getLeadTime() > 0) {
+			cal.add(Calendar.MINUTE, -assessmentMode.getLeadTime());
+		}
+		cal.set(Calendar.SECOND, 0);
+		cal.set(Calendar.MILLISECOND, 0);
+		((AssessmentModeImpl)assessmentMode).setBeginWithLeadTime(cal.getTime());
+		
 		if(assessmentMode.getKey() == null) {
 			dbInstance.getCurrentEntityManager().persist(assessmentMode);
 			reloadedMode = assessmentMode;
@@ -230,4 +248,38 @@ public class AssessmentModeManagerImpl implements AssessmentModeManager {
 		dbInstance.getCurrentEntityManager().persist(modeToArea);
 		return modeToArea;
 	}
+	
+	@Override
+	public boolean isIpAllowed(String ipList, String address) {
+		boolean allOk = false;
+		if(!StringHelper.containsNonWhitespace(ipList)) {
+			allOk |= true;
+		} else {
+			for(StringTokenizer tokenizer = new StringTokenizer(ipList, "\n\r", false); tokenizer.hasMoreTokens(); ) {
+				String ipRange = tokenizer.nextToken();
+				if(StringHelper.containsNonWhitespace(ipRange)) {
+					int indexMask = ipRange.indexOf("/");
+					int indexPseudoRange = ipRange.indexOf("-");
+					if(indexMask > 0) {
+						allOk |= IPUtils.isValidRange(ipRange, address);
+					} else if(indexPseudoRange > 0) {
+						String begin = ipRange.substring(0, indexPseudoRange).trim();
+						String end = ipRange.substring(indexPseudoRange + 1).trim();
+						allOk |= IPUtils.isValidRange(begin, end, address);
+					} else {
+						allOk |= ipRange.equals(address);
+					}
+				}
+			}
+		}
+		return allOk;
+	}
+
+	@Override
+	public boolean isSafelyAllowed(HttpServletRequest request, String safeExamBrowserKey) {
+		String safeExamHash = request.getHeader("x-safeexambrowser-requesthash");
+		String url = request.getRequestURL().toString();
+		String hash = Encoder.sha256Exam(url + safeExamBrowserKey);
+		return safeExamHash != null && safeExamHash.equals(hash);
+	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/assessment/manager/IPRangeChecker.java b/src/main/java/org/olat/course/assessment/manager/IPRangeChecker.java
deleted file mode 100644
index dec104a2f9d..00000000000
--- a/src/main/java/org/olat/course/assessment/manager/IPRangeChecker.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * <a href="http://www.openolat.org">
- * OpenOLAT - Online Learning and Training</a><br>
- * <p>
- * Licensed under the Apache License, Version 2.0 (the "License"); <br>
- * you may not use this file except in compliance with the License.<br>
- * You may obtain a copy of the License at the
- * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
- * <p>
- * Unless required by applicable law or agreed to in writing,<br>
- * software distributed under the License is distributed on an "AS IS" BASIS, <br>
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
- * See the License for the specific language governing permissions and <br>
- * limitations under the License.
- * <p>
- * Initial code contributed and copyrighted by<br>
- * frentix GmbH, http://www.frentix.com
- * <p>
- */
-package org.olat.course.assessment.manager;
-
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-/**
- * 
- * Thanks: https://gist.github.com/madan712/6651967
- * 
- * Initial date: 18.12.2014<br>
- * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
- *
- */
-public class IPRangeChecker {
-	
-	public static long ipToLong(InetAddress ip) {
-		byte[] octets = ip.getAddress();
-		long result = 0;
-		for (byte octet : octets) {
-			result <<= 8;
-			result |= octet & 0xff;
-		}
-		return result;
-	}
- 
-	/**
-	 * TODO replace perhaps with guava, jdk make a round trip to the address.
-	 * @param ipStart
-	 * @param ipEnd
-	 * @param ipToCheck
-	 * @return
-	 */
-	public static boolean isValidRange(String ipStart, String ipEnd, String ipToCheck) {
-		try {
-			long ipLo = ipToLong(InetAddress.getByName(ipStart));
-			long ipHi = ipToLong(InetAddress.getByName(ipEnd));
-			long ipToTest = ipToLong(InetAddress.getByName(ipToCheck));
-			return (ipToTest >= ipLo && ipToTest <= ipHi);
-		} catch (UnknownHostException e) {
-			e.printStackTrace();
-			return false;
-		}
-	}
-}
diff --git a/src/main/java/org/olat/course/assessment/model/AssessmentModeImpl.java b/src/main/java/org/olat/course/assessment/model/AssessmentModeImpl.java
index 2109941c422..8be4f27dfa6 100644
--- a/src/main/java/org/olat/course/assessment/model/AssessmentModeImpl.java
+++ b/src/main/java/org/olat/course/assessment/model/AssessmentModeImpl.java
@@ -54,7 +54,7 @@ import org.olat.repository.RepositoryEntry;
 @Table(name="o_as_mode_course")
 @NamedQueries({
 	@NamedQuery(name="assessmentModeByRepoEntry", query="select mode from courseassessmentmode mode where mode.repositoryEntry.key=:entryKey"),
-	@NamedQuery(name="currentAssessmentModes", query="select mode from courseassessmentmode mode where mode.begin<=:now and mode.end>=:now")
+	@NamedQuery(name="currentAssessmentModes", query="select mode from courseassessmentmode mode where mode.beginWithLeadTime<=:now and mode.end>=:now")
 })
 public class AssessmentModeImpl implements Persistable, AssessmentMode {
 
@@ -86,6 +86,9 @@ public class AssessmentModeImpl implements Persistable, AssessmentMode {
 	private Date end;
 	@Column(name="a_leadtime", nullable=true, insertable=true, updatable=true)
 	private int leadTime;
+	@Temporal(TemporalType.TIMESTAMP)
+	@Column(name="a_begin_with_leadtime", nullable=false, insertable=true, updatable=true)
+	private Date beginWithLeadTime;
 	
 	@Column(name="a_targetaudience", nullable=true, insertable=true, updatable=true)
 	private String targetAudienceString;
@@ -100,6 +103,9 @@ public class AssessmentModeImpl implements Persistable, AssessmentMode {
 	private boolean restrictAccessElements;
 	@Column(name="a_elements", nullable=true, insertable=true, updatable=true)
 	private String elementList;
+	@Column(name="a_start_element", nullable=true, insertable=true, updatable=true)
+	private String startElement;
+	
 
 	@Column(name="a_restrictaccessips", nullable=true, insertable=true, updatable=true)
 	private boolean restrictAccessIps;
@@ -199,6 +205,14 @@ public class AssessmentModeImpl implements Persistable, AssessmentMode {
 		this.leadTime = leadTime;
 	}
 
+	public Date getBeginWithLeadTime() {
+		return beginWithLeadTime;
+	}
+
+	public void setBeginWithLeadTime(Date beginWithLeadTime) {
+		this.beginWithLeadTime = beginWithLeadTime;
+	}
+
 	@Override
 	public Target getTargetAudience() {
 		return targetAudienceString == null || targetAudienceString.isEmpty()
@@ -258,6 +272,16 @@ public class AssessmentModeImpl implements Persistable, AssessmentMode {
 		this.elementList = elementList;
 	}
 
+	@Override
+	public String getStartElement() {
+		return startElement;
+	}
+
+	@Override
+	public void setStartElement(String startElement) {
+		this.startElement = startElement;
+	}
+
 	@Override
 	public boolean isRestrictAccessIps() {
 		return restrictAccessIps;
diff --git a/src/main/java/org/olat/course/assessment/model/TransientAssessmentMode.java b/src/main/java/org/olat/course/assessment/model/TransientAssessmentMode.java
index 85b074bdfe0..cdf2fc20fe8 100644
--- a/src/main/java/org/olat/course/assessment/model/TransientAssessmentMode.java
+++ b/src/main/java/org/olat/course/assessment/model/TransientAssessmentMode.java
@@ -44,8 +44,14 @@ public class TransientAssessmentMode implements Serializable {
 	private String name;
 	private String description;
 	private Date begin;
+	private Date beginWithLeadTime;
 	private Date end;
 	private int leadTime;
+	private String startElementKey;
+	
+	private String ipList;
+	private String safeExamBrowserKey;
+	private String safeExamBrowserHint;
 	
 	public TransientAssessmentMode(AssessmentMode mode) {
 		displayName = mode.getRepositoryEntry().getDisplayname();
@@ -55,8 +61,19 @@ public class TransientAssessmentMode implements Serializable {
 		name = mode.getName();
 		description = mode.getDescription();
 		begin = mode.getBegin();
+		beginWithLeadTime = mode.getBeginWithLeadTime();
 		end = mode.getEnd();
 		leadTime = mode.getLeadTime();
+		startElementKey = mode.getStartElement();
+
+		if(mode.isRestrictAccessIps()) {
+			ipList = mode.getIpList();
+		}
+		
+		if(mode.isSafeExamBrowser()) {
+			safeExamBrowserKey = mode.getSafeExamBrowserKey();
+			safeExamBrowserHint = mode.getSafeExamBrowserHint();
+		}
 	}
 	
 	public static List<TransientAssessmentMode> create(List<AssessmentMode> modes) {
@@ -103,6 +120,30 @@ public class TransientAssessmentMode implements Serializable {
 		this.begin = begin;
 	}
 
+	public Date getBeginWithLeadTime() {
+		return beginWithLeadTime;
+	}
+
+	public void setBeginWithLeadTime(Date beginWithLeadTime) {
+		this.beginWithLeadTime = beginWithLeadTime;
+	}
+
+	public String getSafeExamBrowserKey() {
+		return safeExamBrowserKey;
+	}
+
+	public void setSafeExamBrowserKey(String safeExamBrowserKey) {
+		this.safeExamBrowserKey = safeExamBrowserKey;
+	}
+
+	public String getSafeExamBrowserHint() {
+		return safeExamBrowserHint;
+	}
+
+	public void setSafeExamBrowserHint(String safeExamBrowserHint) {
+		this.safeExamBrowserHint = safeExamBrowserHint;
+	}
+
 	public Date getEnd() {
 		return end;
 	}
@@ -118,4 +159,17 @@ public class TransientAssessmentMode implements Serializable {
 	public void setLeadTime(int leadTime) {
 		this.leadTime = leadTime;
 	}
+
+	public String getStartElementKey() {
+		return startElementKey;
+	}
+
+	public void setStartElementKey(String startElementKey) {
+		this.startElementKey = startElementKey;
+	}
+	
+	public String getIpList() {
+		return ipList;
+	}
+	
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/assessment/ui/AssessmentModeEditController.java b/src/main/java/org/olat/course/assessment/ui/AssessmentModeEditController.java
index e1ed6caf2f3..eca19748ecd 100644
--- a/src/main/java/org/olat/course/assessment/ui/AssessmentModeEditController.java
+++ b/src/main/java/org/olat/course/assessment/ui/AssessmentModeEditController.java
@@ -34,6 +34,7 @@ import org.olat.core.gui.components.form.flexible.elements.IntegerElement;
 import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
 import org.olat.core.gui.components.form.flexible.elements.RichTextElement;
 import org.olat.core.gui.components.form.flexible.elements.SingleSelection;
+import org.olat.core.gui.components.form.flexible.elements.StaticTextElement;
 import org.olat.core.gui.components.form.flexible.elements.TextElement;
 import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
 import org.olat.core.gui.components.form.flexible.impl.FormEvent;
@@ -78,7 +79,8 @@ public class AssessmentModeEditController extends FormBasicController {
 	private SingleSelection targetEl;
 	private IntegerElement leadTimeEl;
 	private DateChooser beginEl, endEl;
-	private FormLink chooseGroupsButton, chooseAreasButton, chooseElementsButton;
+	private StaticTextElement startElementEl;
+	private FormLink chooseGroupsButton, chooseAreasButton, chooseStartElementButton, chooseElementsButton;
 	private TextElement nameEl, ipListEl, safeExamBrowserKeyEl;
 	private RichTextElement descriptionEl, safeExamBrowserHintEl;
 	private FormLayoutContainer chooseGroupsCont, chooseElementsCont;
@@ -88,6 +90,7 @@ public class AssessmentModeEditController extends FormBasicController {
 	private AreaSelectionController areaChooseCtrl;
 	private GroupSelectionController groupChooseCtrl;
 	private ChooseElementsController chooseElementsCtrl;
+	private ChooseStartElementController chooseStartElementCtrl;
 	
 	private List<Long> areaKeys;
 	private List<String> areaNames;
@@ -95,6 +98,7 @@ public class AssessmentModeEditController extends FormBasicController {
 	private List<String> groupNames;
 	private List<String> elementKeys;
 	private List<String> elementNames;
+	private String startElementKey;
 	
 	private AssessmentMode assessmentMode;
 	private final OLATResourceable courseOres;
@@ -231,6 +235,14 @@ public class AssessmentModeEditController extends FormBasicController {
 		
 		chooseElementsButton = uifactory.addFormLink("choose.elements", chooseElementsCont, Link.BUTTON);
 		
+		startElementKey = assessmentMode.getStartElement();
+		String startElementName = "";
+		if(StringHelper.containsNonWhitespace(startElementKey)) {
+			startElementName = getCourseNodeName(startElementKey, treeModel);
+		}
+		startElementEl = uifactory.addStaticTextElement("mode.start.element", "mode.start.element", startElementName, formLayout);
+		chooseStartElementButton = uifactory.addFormLink("choose.start.element", formLayout, Link.BUTTON);
+
 		//ips
 		ipsEl = uifactory.addCheckboxesHorizontal("ips", "mode.ips", formLayout, onKeys, onValues);
 		ipsEl.select(onKeys[0], assessmentMode.isRestrictAccessIps());
@@ -297,6 +309,14 @@ public class AssessmentModeEditController extends FormBasicController {
 			}
 			cmc.deactivate();
 			cleanUp();
+		} else if(chooseStartElementCtrl == source) {
+			if(Event.DONE_EVENT == event || Event.CHANGED_EVENT == event) {
+				startElementKey = chooseStartElementCtrl.getSelectedKey();
+				String elementName = chooseStartElementCtrl.getSelectedName();
+				startElementEl.setValue(elementName);
+			}
+			cmc.deactivate();
+			cleanUp();
 		} else if(cmc == source) {
 			cmc.deactivate();
 		}
@@ -304,10 +324,12 @@ public class AssessmentModeEditController extends FormBasicController {
 	}
 	
 	private void cleanUp() {
+		removeAsListenerAndDispose(chooseStartElementCtrl);
 		removeAsListenerAndDispose(chooseElementsCtrl);
 		removeAsListenerAndDispose(groupChooseCtrl);
 		removeAsListenerAndDispose(areaChooseCtrl);
 		removeAsListenerAndDispose(cmc);
+		chooseStartElementCtrl = null;
 		chooseElementsCtrl = null;
 		groupChooseCtrl = null;
 		areaChooseCtrl = null;
@@ -375,6 +397,12 @@ public class AssessmentModeEditController extends FormBasicController {
 			assessmentMode.setElementList(null);
 		}
 		
+		if(StringHelper.containsNonWhitespace(startElementKey)) {
+			assessmentMode.setStartElement(startElementKey);
+		} else {
+			assessmentMode.setStartElement(null);
+		}
+		
 		boolean ipRestriction = ipsEl.isAtLeastSelected(1);
 		assessmentMode.setRestrictAccessIps(ipRestriction);
 		if(ipRestriction) {
@@ -471,6 +499,8 @@ public class AssessmentModeEditController extends FormBasicController {
 			doChooseGroups(ureq);
 		} else if(chooseElementsButton == source) {
 			doChooseElements(ureq);
+		} else if(chooseStartElementButton == source) {
+			doChooseStartElement(ureq);
 		}
 		
 		super.formInnerEvent(ureq, source, event);
@@ -488,7 +518,19 @@ public class AssessmentModeEditController extends FormBasicController {
 		listenTo(chooseElementsCtrl);
 		
 		cmc = new CloseableModalController(getWindowControl(), "close", chooseElementsCtrl.getInitialComponent(),
-				true, getTranslator().translate("popup.chooseareas"));
+				true, getTranslator().translate("popup.chooseelements"));
+		listenTo(cmc);
+		cmc.activate();
+	}
+	
+	private void doChooseStartElement(UserRequest ureq) {
+		if(chooseElementsCtrl != null) return;
+
+		chooseStartElementCtrl = new ChooseStartElementController(ureq, getWindowControl(), startElementKey, courseOres);
+		listenTo(chooseStartElementCtrl);
+		
+		cmc = new CloseableModalController(getWindowControl(), "close", chooseStartElementCtrl.getInitialComponent(),
+				true, getTranslator().translate("popup.choosestartelement"));
 		listenTo(cmc);
 		cmc.activate();
 	}
diff --git a/src/main/java/org/olat/course/assessment/ui/AssessmentModeUserConfirmationController.java b/src/main/java/org/olat/course/assessment/ui/AssessmentModeUserConfirmationController.java
index 5dd63332efb..a9b1a2259fd 100644
--- a/src/main/java/org/olat/course/assessment/ui/AssessmentModeUserConfirmationController.java
+++ b/src/main/java/org/olat/course/assessment/ui/AssessmentModeUserConfirmationController.java
@@ -20,6 +20,7 @@
 package org.olat.course.assessment.ui;
 
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 
@@ -38,7 +39,10 @@ import org.olat.core.gui.control.generic.closablewrapper.CloseableModalControlle
 import org.olat.core.id.OLATResourceable;
 import org.olat.core.util.CodeHelper;
 import org.olat.core.util.Formatter;
+import org.olat.core.util.StringHelper;
+import org.olat.course.assessment.AssessmentModeManager;
 import org.olat.course.assessment.model.TransientAssessmentMode;
+import org.springframework.beans.factory.annotation.Autowired;
 
 /**
  * 
@@ -48,29 +52,90 @@ import org.olat.course.assessment.model.TransientAssessmentMode;
  */
 public class AssessmentModeUserConfirmationController extends BasicController {
 
+	private final VelocityContainer mainVC;
 	private final CloseableModalController cmc;
 	
+	private final String address;
+	
+	@Autowired
+	private AssessmentModeManager assessmentmodeMgr;
+	
 	public AssessmentModeUserConfirmationController(UserRequest ureq, WindowControl wControl, List<TransientAssessmentMode> modes) {
 		super(ureq, wControl);
 		putInitialPanel(new Panel("assessment-mode-chooser"));
+
+		System.out.println("ureq: " + ureq);
+		System.out.println("hreq: " + ureq.getHttpReq());
+		address = ureq.getHttpReq().getRemoteAddr();
 		
-		VelocityContainer mainVC = createVelocityContainer("choose_mode");
+		mainVC = createVelocityContainer("choose_mode");
 		List<Mode> modeWrappers = new ArrayList<Mode>();
 		for(TransientAssessmentMode mode:modes) {
-			String name = "go-" + CodeHelper.getRAMUniqueID();
-			Link button = LinkFactory.createCustomLink(name, "go", "current.mode.start", Link.BUTTON, mainVC, this);
-			button.setUserObject(mode);
-			
-			Mode wrapper = new Mode(button, mode, getLocale());
-			modeWrappers.add(wrapper);
+			Mode wrapper = initAssessmentMode(ureq, mode);
+			if(wrapper != null) {
+				modeWrappers.add(wrapper);
+			}
 		}
 		mainVC.contextPut("modeWrappers", modeWrappers);
+		//check
 
 		cmc = new CloseableModalController(getWindowControl(), translate("close"), mainVC, true, translate("current.mode"), false);	
 		cmc.activate();
 		listenTo(cmc);
 	}
 	
+	private Mode initAssessmentMode(UserRequest ureq, TransientAssessmentMode mode) {
+		Date now = new Date();
+		
+		Status state = null;
+		
+		Date beginWithLeadTime = mode.getBeginWithLeadTime();
+		Date begin = mode.getBegin();
+		Date end = mode.getEnd();
+		
+		if(begin.after(now)) {
+			return null;
+		} else if(beginWithLeadTime.before(now) && begin.after(now)) {
+			state = Status.wait;
+		} else if(begin.before(now) && end.after(now)) {
+			state = Status.allowed;
+		} else if(end.before(now)) {
+			return null;
+		}
+
+		StringBuilder sb = new StringBuilder();
+
+		boolean allowed = true;
+		if(mode.getIpList() != null) {
+			boolean ipInRange = assessmentmodeMgr.isIpAllowed(mode.getIpList(), address);
+			if(!ipInRange) {
+				sb.append(translate("error.ip.range"));
+			}
+			allowed &= ipInRange;
+		}
+		if(mode.getSafeExamBrowserKey() != null) {
+			boolean safeExamCheck = assessmentmodeMgr.isSafelyAllowed(ureq.getHttpReq(), mode.getSafeExamBrowserKey());
+			if(!safeExamCheck) {
+				if(sb.length() > 0) sb.append("<br />");
+				sb.append(translate("error.safe.exam"));
+			}
+			allowed &= safeExamCheck;
+		}
+
+		Link button = null;
+		if(allowed) {
+			String name = "go-" + CodeHelper.getRAMUniqueID();
+			button = LinkFactory.createCustomLink(name, "go", "current.mode.start", Link.BUTTON, mainVC, this);
+			button.setCustomEnabledLinkCSS("btn btn-primary");
+			button.setUserObject(mode);
+			button.setEnabled(state == Status.allowed);	
+		} else {
+			state = Status.refused;
+		}
+		
+		return new Mode(button, state.name(), sb.toString(), mode, getLocale());
+	}
+	
 	@Override
 	protected void doDispose() {
 		//
@@ -100,19 +165,33 @@ public class AssessmentModeUserConfirmationController extends BasicController {
 		Windows.getWindows(ureq).getChiefController().lockResource(resource);
 		fireEvent(ureq, new ChooseAssessmentModeEvent(mode));
 		
-		String businessPath = "[RepositoryEntry:" + mode.getRepositoryEntryKey() + "]"; //TODO node
+		String businessPath = "[RepositoryEntry:" + mode.getRepositoryEntryKey() + "]";
+		if(StringHelper.containsNonWhitespace(mode.getStartElementKey())) {
+			businessPath += "[CourseNode:" + mode.getStartElementKey() + "]";
+		}
 		NewControllerFactory.getInstance().launch(businessPath, ureq, getWindowControl());
 	}
 	
+	public static enum Status {
+		refused,
+		allowed,
+		wait,
+		closed
+	}
+	
 	public static final class Mode {
-		
+
+		private String status;
+		private String errors;
 		private final Locale locale;
 		private final Link goButton;
 		private final TransientAssessmentMode mode;
 		
-		public Mode(Link goButton, TransientAssessmentMode mode, Locale locale) {
+		public Mode(Link goButton, String status, String errors, TransientAssessmentMode mode, Locale locale) {
 			this.goButton = goButton;
 			this.mode = mode;
+			this.errors = errors;
+			this.status = status;
 			this.locale = locale;
 		}
 		
@@ -124,10 +203,30 @@ public class AssessmentModeUserConfirmationController extends BasicController {
 			return mode.getDescription();
 		}
 		
+		public String getSafeExamBrowserHint() {
+			return mode.getSafeExamBrowserHint();
+		}
+		
 		public String getDisplayName() {
 			return mode.getDisplayName();
 		}
 		
+		public String getStatus() {
+			return status;
+		}
+
+		public void setStatus(String status) {
+			this.status = status;
+		}
+
+		public String getErrors() {
+			return errors;
+		}
+
+		public void setErrors(String errors) {
+			this.errors = errors;
+		}
+
 		public String getBegin() {
 			return Formatter.getInstance(locale).formatDateAndTime(mode.getBegin());
 		}
diff --git a/src/main/java/org/olat/course/assessment/ui/ChooseStartElementController.java b/src/main/java/org/olat/course/assessment/ui/ChooseStartElementController.java
new file mode 100644
index 00000000000..1b233aedddd
--- /dev/null
+++ b/src/main/java/org/olat/course/assessment/ui/ChooseStartElementController.java
@@ -0,0 +1,101 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.assessment.ui;
+
+import java.util.Collections;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.tree.MenuTreeItem;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.Event;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.id.OLATResourceable;
+import org.olat.core.util.resource.OresHelper;
+import org.olat.course.CourseFactory;
+import org.olat.course.ICourse;
+import org.olat.course.nodes.CourseNode;
+import org.olat.course.tree.CourseEditorTreeModel;
+
+/**
+ * 
+ * Initial date: 19.12.2014<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class ChooseStartElementController extends FormBasicController {
+
+	private MenuTreeItem selectTree;
+	private CourseEditorTreeModel treeModel;
+
+	private final OLATResourceable ores;
+	private final String preSelectedKey;
+
+	public ChooseStartElementController(UserRequest ureq, WindowControl wControl, String selectedKey, OLATResourceable ores) {
+		super(ureq, wControl, "course_element");
+		this.ores = OresHelper.clone(ores);
+		preSelectedKey = selectedKey;
+		initForm(ureq);
+	}
+	
+	@Override
+	protected void doDispose() {
+		// nothing to dispose
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		ICourse course = CourseFactory.loadCourse(ores);
+		treeModel = course.getEditorTreeModel();
+		selectTree = uifactory.addTreeMultiselect("elements", null, formLayout, treeModel, this);
+		selectTree.setSelectedKeys(Collections.singletonList(preSelectedKey));
+		
+		uifactory.addFormCancelButton("cancel", formLayout, ureq, getWindowControl());
+		uifactory.addFormSubmitButton("ok", formLayout);
+	}
+	
+	public String getSelectedKey() {
+		return selectTree.getSelectedNodeId();
+	}
+	
+	public String getSelectedName() {
+		String selectedKey = getSelectedKey();
+		String name = null;
+
+		CourseNode node = treeModel.getCourseNode(selectedKey);
+		if(node == null) {
+			//not published??
+		} else {
+			name = node.getShortTitle();
+		}
+		return name;
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		fireEvent(ureq, Event.DONE_EVENT);
+	}
+
+	@Override
+	protected void formCancelled(UserRequest ureq) {
+		fireEvent(ureq, Event.CANCELLED_EVENT);
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/assessment/ui/_content/choose_mode.html b/src/main/java/org/olat/course/assessment/ui/_content/choose_mode.html
index 4506849bc6e..471dd31d3d3 100644
--- a/src/main/java/org/olat/course/assessment/ui/_content/choose_mode.html
+++ b/src/main/java/org/olat/course/assessment/ui/_content/choose_mode.html
@@ -6,7 +6,15 @@
 			<br/><em>$r.translate("current.mode.leadtime", $mode.leadTime)</em>
 		#end
 	</p>
-	<p>$mode.description</p>
-	<div class="o_button_group">$r.render($mode.buttonName)</div>
-#end
-
+	#if($mode.description && !$mode.description.empty)
+		<p>$mode.description</p>
+	#end
+	#if($mode.safeExamBrowserHint && !$mode.safeExamBrowserHint.empty)
+		<p class="o_info">$mode.safeExamBrowserHint</p>
+	#end
+	#if($mode.status == "allowed" || $mode.status == "wait")
+		<div class="o_button_group">$r.render($mode.buttonName)</div>
+	#else
+		<p class="o_error">$mode.errors</p>
+	#end
+#end
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/assessment/ui/_content/course_element.html b/src/main/java/org/olat/course/assessment/ui/_content/course_element.html
new file mode 100644
index 00000000000..b02c1900d53
--- /dev/null
+++ b/src/main/java/org/olat/course/assessment/ui/_content/course_element.html
@@ -0,0 +1,4 @@
+$r.render("elements")
+<div class="o_button_group">
+	$r.render("ok") $r.render("cancel")
+</div>
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/assessment/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/assessment/ui/_i18n/LocalStrings_de.properties
index a327692a1d9..eedef79e040 100644
--- a/src/main/java/org/olat/course/assessment/ui/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/course/assessment/ui/_i18n/LocalStrings_de.properties
@@ -4,6 +4,7 @@ areas=Lernbereich
 choose.areas=Lernbereich auswählen
 choose.elements=Kursbausteine auswählen
 choose.groups=Gruppen auswählen
+choose.start.element=Kursbaustein auswählen
 current.mode=Aktuelle Prüfung
 current.mode.desc=Im folgenden Kurs ist für den aktuellen Zeitraum eine Prüfung für Sie vorgesehen. Während dieser Prüfung können keine anderen Funktionen in OpenOLAT verwendet werden:
 current.mode.start=Zur Prüfung
@@ -12,6 +13,8 @@ current.mode.datetimes=Von {0} bis {1}
 current.mode.leadtime=Ab {0} Minuten vor Prüfungsbeginn können keine andere Kurse mehr geöffnet werden.
 delete.mode=Pr\u00FCfungssetting l\u00F6schen
 elements=Kursbausteine
+error.ip.range=Sie sind nicht in dem richtigen Netzwerk.
+error.safe.exam=Bitte, benutzen Sie den Safe Exam Browser.
 form.mode.description=Erstellen Sie ein neues Pr\u00FCfungssetting um f\u00FCr diesen Kurs oder Elemets aus diesem Kurs in einem gesch\u00FCtzten Modus mit einem eingeschr\u00E4nken M\u00F6glichkeiten zu betreiben.
 form.mode.title=Pr\u00FCfungssetting {0}
 form.mode.title.add=Pr\u00FCfungssetting
@@ -28,13 +31,16 @@ mode.for.coach=Pr\u00FCfundssettings auch bei Betreuer anwenden
 mode.description=Beschreibung
 mode.safeexambrowser=Safe exam Browser verwenden
 mode.safeexambrowser.key=Browser Exam key
-mode.safeexambrowser.hint=Safe Exam Browser Hinweis 
+mode.safeexambrowser.hint=Safe Exam Browser Hinweis
+mode.start.element=Startbaustein
 mode.target=Teilnehmer
 modes.title=Pr\u00FCfungsmodus
 modes.description=Erstellen Sie ein neues Pr\u00FCfungssetting um f\u00FCr diesen Kurs oder Elemets aus diesem Kurs in einem gesch\u00FCtzten Modus mit einem eingeschr\u00E4nken M\u00F6glichkeiten zu betreiben.
 new.mode=Neues Pr\u00FCfungssetting
 popup.chooseareas=$\:choose.areas
 popup.choosegroups=$\:choose.groups
+popup.chooseelements=$\:choose.elements
+popup.choosestartelement=Startbaustein auswählen
 table.header.name=Prüfung
 table.header.begin=Von
 table.header.end=Bis
diff --git a/src/main/resources/database/mysql/alter_10_1_0_to_10_2_0.sql b/src/main/resources/database/mysql/alter_10_1_0_to_10_2_0.sql
index 4c481cb7189..b295ba15f22 100644
--- a/src/main/resources/database/mysql/alter_10_1_0_to_10_2_0.sql
+++ b/src/main/resources/database/mysql/alter_10_1_0_to_10_2_0.sql
@@ -5,11 +5,13 @@ create table o_as_mode_course (
    a_name varchar(255),
    a_description longtext,
    a_begin datetime not null,
+   a_begin_with_leadtime datetime not null,
    a_creationdate datetime not null,
    a_leadtime bigint not null default 0,
    a_targetaudience varchar(16),
    a_restrictaccesselements bit not null default 0,
    a_elements varchar(2048),
+   a_start_element varchar(64),
    a_restrictaccessips bit not null default 0,
    a_ips varchar(2048),
    a_safeexambrowser bit not null default 0,
diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql
index 923d5223fb7..c5af60575c6 100644
--- a/src/main/resources/database/mysql/setupDatabase.sql
+++ b/src/main/resources/database/mysql/setupDatabase.sql
@@ -1062,6 +1062,44 @@ create table o_as_user_course_infos (
    primary key (id)
 );
 
+create table o_as_mode_course (
+   id bigint not null,
+   creationdate datetime not null,
+   lastmodified datetime not null,
+   a_name varchar(255),
+   a_description longtext,
+   a_begin datetime not null,
+   a_begin_with_leadtime datetime not null,
+   a_creationdate datetime not null,
+   a_leadtime bigint not null default 0,
+   a_targetaudience varchar(16),
+   a_restrictaccesselements bit not null default 0,
+   a_elements varchar(2048),
+   a_start_element varchar(64),
+   a_restrictaccessips bit not null default 0,
+   a_ips varchar(2048),
+   a_safeexambrowser bit not null default 0,
+   a_safeexambrowserkey varchar(2048),
+   a_safeexambrowserhint longtext,
+   a_applysettingscoach bit not null default 0,
+   fk_entry bigint not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_group (
+   id bigint not null,
+   fk_assessment_mode_id bigint not null,
+   fk_group_id bigint not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_area (
+   id bigint not null,
+   fk_assessment_mode_id bigint not null,
+   fk_area_id bigint not null,
+   primary key (id)
+);
+
 create table o_cer_template (
    id bigint not null,
    creationdate datetime not null,
@@ -1731,6 +1769,9 @@ alter table o_ac_reservation ENGINE = InnoDB;
 alter table o_ac_paypal_transaction ENGINE = InnoDB;
 alter table o_as_eff_statement ENGINE = InnoDB;
 alter table o_as_user_course_infos ENGINE = InnoDB;
+alter table o_as_mode_course ENGINE = InnoDB;
+alter table o_as_mode_course ENGINE = InnoDB;
+alter table o_as_mode_course_to_area ENGINE = InnoDB;
 alter table o_mapper ENGINE = InnoDB;
 alter table o_qp_pool ENGINE = InnoDB;
 alter table o_qp_taxonomy_level ENGINE = InnoDB;
@@ -2067,6 +2108,15 @@ alter table o_qp_item_type add unique (q_type(200));
 alter table o_lti_outcome add constraint idx_lti_outcome_ident_id foreign key (fk_identity_id) references o_bs_identity(id);
 alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk_resource_id) references o_olatresource(resource_id);
 
+-- assessment mode
+alter table o_as_mode_course add constraint as_mode_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
+
+alter table o_as_mode_course_to_group add constraint as_modetogroup_group_idx foreign key (fk_group_id) references o_gp_business (group_id);
+alter table o_as_mode_course_to_group add constraint as_modetogroup_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+
+alter table o_as_mode_course_to_area add constraint as_modetoarea_area_idx foreign key (fk_area_id) references o_gp_bgarea (area_id);
+alter table o_as_mode_course_to_area add constraint as_modetoarea_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+
 -- certificate
 alter table o_cer_certificate add constraint cer_to_identity_idx foreign key (fk_identity) references o_bs_identity (id);
 alter table o_cer_certificate add constraint cer_to_resource_idx foreign key (fk_olatresource) references o_olatresource (resource_id);
diff --git a/src/main/resources/database/oracle/alter_10_1_0_to_10_2_0.sql b/src/main/resources/database/oracle/alter_10_1_0_to_10_2_0.sql
new file mode 100644
index 00000000000..40c0484716a
--- /dev/null
+++ b/src/main/resources/database/oracle/alter_10_1_0_to_10_2_0.sql
@@ -0,0 +1,51 @@
+create table o_as_mode_course (
+   id number(20) not null,
+   creationdate date not null,
+   lastmodified date not null,
+   a_name varchar2(255 char),
+   a_description clob,
+   a_begin date not null,
+   a_begin_with_leadtime date not null,
+   a_creationdate date not null,
+   a_leadtime number(20) default 0 not null,
+   a_targetaudience varchar2(16 char),
+   a_restrictaccesselements number default 0 not null,
+   a_elements varchar2(2048 char),
+   a_start_element varchar2(64 char),
+   a_restrictaccessips number default 0 not null,
+   a_ips varchar2(2048 char),
+   a_safeexambrowser number default 0 not null,
+   a_safeexambrowserkey varchar2(2048 char),
+   a_safeexambrowserhint clob,
+   a_applysettingscoach number default 0 not null,
+   fk_entry number(20) not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_group (
+   id number(20) not null,
+   fk_assessment_mode_id number(20) not null,
+   fk_group_id number(20) not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_area (
+   id number(20) not null,
+   fk_assessment_mode_id number(20) not null,
+   fk_area_id number(20) not null,
+   primary key (id)
+);
+
+alter table o_as_mode_course add constraint as_mode_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
+create index idx_as_mode_to_repo_entry_idx on o_as_mode_course (fk_entry);
+
+alter table o_as_mode_course_to_group add constraint as_modetogroup_group_idx foreign key (fk_group_id) references o_gp_business (group_id);
+alter table o_as_mode_course_to_group add constraint as_modetogroup_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+create index idx_as_modetogroup_group_idx on o_as_mode_course_to_group (fk_group_id);
+create index idx_as_modetogroup_mode_idx on o_as_mode_course_to_group (fk_assessment_mode_id);
+
+alter table o_as_mode_course_to_area add constraint as_modetoarea_area_idx foreign key (fk_area_id) references o_gp_bgarea (area_id);
+alter table o_as_mode_course_to_area add constraint as_modetoarea_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+create index idx_as_modetoarea_area_idx on o_as_mode_course_to_area (fk_area_id);
+create index idx_as_modetoarea_mode_idx on o_as_mode_course_to_area (fk_assessment_mode_id);
+
diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql
index d3e52ca1bb2..ef37fdd9817 100644
--- a/src/main/resources/database/oracle/setupDatabase.sql
+++ b/src/main/resources/database/oracle/setupDatabase.sql
@@ -1096,6 +1096,44 @@ create table o_as_user_course_infos (
    primary key (id)
 );
 
+create table o_as_mode_course (
+   id number(20) not null,
+   creationdate date not null,
+   lastmodified date not null,
+   a_name varchar2(255 char),
+   a_description clob,
+   a_begin date not null,
+   a_begin_with_leadtime date not null,
+   a_creationdate date not null,
+   a_leadtime number(20) default 0 not null,
+   a_targetaudience varchar2(16 char),
+   a_restrictaccesselements number default 0 not null,
+   a_elements varchar2(2048 char),
+   a_start_element varchar2(64 char),
+   a_restrictaccessips number default 0 not null,
+   a_ips varchar2(2048 char),
+   a_safeexambrowser number default 0 not null,
+   a_safeexambrowserkey varchar2(2048 char),
+   a_safeexambrowserhint clob,
+   a_applysettingscoach number default 0 not null,
+   fk_entry number(20) not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_group (
+   id number(20) not null,
+   fk_assessment_mode_id number(20) not null,
+   fk_group_id number(20) not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_area (
+   id number(20) not null,
+   fk_assessment_mode_id number(20) not null,
+   fk_area_id number(20) not null,
+   primary key (id)
+);
+
 create table o_cer_template (
    id number(20) not null,
    creationdate date not null,
@@ -2234,7 +2272,21 @@ create index idx_lti_outcome_ident_id_idx on o_lti_outcome (fk_identity_id);
 alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk_resource_id) references o_olatresource(resource_id);
 create index idx_lti_outcome_rsrc_id_idx on o_lti_outcome (fk_resource_id);
 
---certificates
+-- assessment mode
+alter table o_as_mode_course add constraint as_mode_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
+create index idx_as_mode_to_repo_entry_idx on o_as_mode_course (fk_entry);
+
+alter table o_as_mode_course_to_group add constraint as_modetogroup_group_idx foreign key (fk_group_id) references o_gp_business (group_id);
+alter table o_as_mode_course_to_group add constraint as_modetogroup_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+create index idx_as_modetogroup_group_idx on o_as_mode_course_to_group (fk_group_id);
+create index idx_as_modetogroup_mode_idx on o_as_mode_course_to_group (fk_assessment_mode_id);
+
+alter table o_as_mode_course_to_area add constraint as_modetoarea_area_idx foreign key (fk_area_id) references o_gp_bgarea (area_id);
+alter table o_as_mode_course_to_area add constraint as_modetoarea_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+create index idx_as_modetoarea_area_idx on o_as_mode_course_to_area (fk_area_id);
+create index idx_as_modetoarea_mode_idx on o_as_mode_course_to_area (fk_assessment_mode_id);
+
+-- certificates
 alter table o_cer_certificate add constraint cer_to_identity_idx foreign key (fk_identity) references o_bs_identity (id);
 create index cer_identity_idx on o_cer_certificate (fk_identity);
 alter table o_cer_certificate add constraint cer_to_resource_idx foreign key (fk_olatresource) references o_olatresource (resource_id);
diff --git a/src/main/resources/database/postgresql/alter_10_1_0_to_10_2_0.sql b/src/main/resources/database/postgresql/alter_10_1_0_to_10_2_0.sql
new file mode 100644
index 00000000000..b7fe3089144
--- /dev/null
+++ b/src/main/resources/database/postgresql/alter_10_1_0_to_10_2_0.sql
@@ -0,0 +1,51 @@
+create table o_as_mode_course (
+   id int8 not null,
+   creationdate timestamp not null,
+   lastmodified timestamp not null,
+   a_name varchar(255),
+   a_description text,
+   a_begin timestamp not null,
+   a_begin_with_leadtime timestamp not null,
+   a_creationdate timestamp not null,
+   a_leadtime int8 not null default 0,
+   a_targetaudience varchar(16),
+   a_restrictaccesselements bool not null default false,
+   a_elements varchar(2048),
+   a_start_element varchar(64),
+   a_restrictaccessips bool not null default false,
+   a_ips varchar(2048),
+   a_safeexambrowser bool not null default false,
+   a_safeexambrowserkey varchar(2048),
+   a_safeexambrowserhint text,
+   a_applysettingscoach bool not null default false,
+   fk_entry int8 not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_group (
+   id int8 not null,
+   fk_assessment_mode_id int8 not null,
+   fk_group_id int8 not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_area (
+   id int8 not null,
+   fk_assessment_mode_id int8 not null,
+   fk_area_id int8 not null,
+   primary key (id)
+);
+
+alter table o_as_mode_course add constraint as_mode_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
+create index idx_as_mode_to_repo_entry_idx on o_as_mode_course (fk_entry);
+
+alter table o_as_mode_course_to_group add constraint as_modetogroup_group_idx foreign key (fk_group_id) references o_gp_business (group_id);
+alter table o_as_mode_course_to_group add constraint as_modetogroup_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+create index idx_as_modetogroup_group_idx on o_as_mode_course_to_group (fk_group_id);
+create index idx_as_modetogroup_mode_idx on o_as_mode_course_to_group (fk_assessment_mode_id);
+
+alter table o_as_mode_course_to_area add constraint as_modetoarea_area_idx foreign key (fk_area_id) references o_gp_bgarea (area_id);
+alter table o_as_mode_course_to_area add constraint as_modetoarea_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+create index idx_as_modetoarea_area_idx on o_as_mode_course_to_area (fk_area_id);
+create index idx_as_modetoarea_mode_idx on o_as_mode_course_to_area (fk_assessment_mode_id);
+
diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql
index 188a862e662..675c13f11d4 100644
--- a/src/main/resources/database/postgresql/setupDatabase.sql
+++ b/src/main/resources/database/postgresql/setupDatabase.sql
@@ -1063,6 +1063,44 @@ create table o_as_user_course_infos (
    primary key (id)
 );
 
+create table o_as_mode_course (
+   id int8 not null,
+   creationdate timestamp not null,
+   lastmodified timestamp not null,
+   a_name varchar(255),
+   a_description text,
+   a_begin timestamp not null,
+   a_begin_with_leadtime timestamp not null,
+   a_creationdate timestamp not null,
+   a_leadtime int8 not null default 0,
+   a_targetaudience varchar(16),
+   a_restrictaccesselements bool not null default false,
+   a_elements varchar(2048),
+   a_start_element varchar(64),
+   a_restrictaccessips bool not null default false,
+   a_ips varchar(2048),
+   a_safeexambrowser bool not null default false,
+   a_safeexambrowserkey varchar(2048),
+   a_safeexambrowserhint text,
+   a_applysettingscoach bool not null default false,
+   fk_entry int8 not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_group (
+   id int8 not null,
+   fk_assessment_mode_id int8 not null,
+   fk_group_id int8 not null,
+   primary key (id)
+);
+
+create table o_as_mode_course_to_area (
+   id int8 not null,
+   fk_assessment_mode_id int8 not null,
+   fk_area_id int8 not null,
+   primary key (id)
+);
+
 create table o_cer_template (
    id int8 not null,
    creationdate timestamp not null,
@@ -2081,6 +2119,20 @@ create index idx_lti_outcome_ident_id_idx on o_lti_outcome (fk_identity_id);
 alter table o_lti_outcome add constraint idx_lti_outcome_rsrc_id foreign key (fk_resource_id) references o_olatresource(resource_id);
 create index idx_lti_outcome_rsrc_id_idx on o_lti_outcome (fk_resource_id);
 
+-- assessment mode
+alter table o_as_mode_course add constraint as_mode_to_repo_entry_idx foreign key (fk_entry) references o_repositoryentry (repositoryentry_id);
+create index idx_as_mode_to_repo_entry_idx on o_as_mode_course (fk_entry);
+
+alter table o_as_mode_course_to_group add constraint as_modetogroup_group_idx foreign key (fk_group_id) references o_gp_business (group_id);
+alter table o_as_mode_course_to_group add constraint as_modetogroup_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+create index idx_as_modetogroup_group_idx on o_as_mode_course_to_group (fk_group_id);
+create index idx_as_modetogroup_mode_idx on o_as_mode_course_to_group (fk_assessment_mode_id);
+
+alter table o_as_mode_course_to_area add constraint as_modetoarea_area_idx foreign key (fk_area_id) references o_gp_bgarea (area_id);
+alter table o_as_mode_course_to_area add constraint as_modetoarea_mode_idx foreign key (fk_assessment_mode_id) references o_as_mode_course (id);
+create index idx_as_modetoarea_area_idx on o_as_mode_course_to_area (fk_area_id);
+create index idx_as_modetoarea_mode_idx on o_as_mode_course_to_area (fk_assessment_mode_id);
+
 -- certificates
 alter table o_cer_certificate add constraint cer_to_identity_idx foreign key (fk_identity) references o_bs_identity (id);
 create index cer_identity_idx on o_cer_certificate (fk_identity);
diff --git a/src/test/java/org/olat/core/util/IPUtilsTest.java b/src/test/java/org/olat/core/util/IPUtilsTest.java
new file mode 100644
index 00000000000..1fb4707a0e6
--- /dev/null
+++ b/src/test/java/org/olat/core/util/IPUtilsTest.java
@@ -0,0 +1,41 @@
+package org.olat.core.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * 
+ * Initial date: 22.12.2014<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class IPUtilsTest {
+	
+	@Test
+	public void checkRange_start_end() {
+		String start = "192.168.5.5";
+		String end = "192.168.5.25";
+		
+		boolean  check1 = IPUtils.isValidRange(start, end, "192.168.5.21");
+		Assert.assertTrue(check1);
+		
+		boolean  check2 = IPUtils.isValidRange(start, end, "192.168.5.45");
+		Assert.assertFalse(check2);
+	}
+	/*
+	@Test
+	public void checkRange_mask_31() {
+		String ipWithMask = "192.168.100.1/24";
+		
+		boolean  allowed1 = IPUtils.isValidRange(ipWithMask, "192.168.100.1");
+		Assert.assertTrue(allowed1);
+
+		boolean  notAllowed1 = IPUtils.isValidRange(ipWithMask, "192.168.99.255");
+		Assert.assertFalse(notAllowed1);
+		boolean  notAllowed2 = IPUtils.isValidRange(ipWithMask, "192.168.101.1");
+		Assert.assertFalse(notAllowed2);
+		boolean  notAllowed3 = IPUtils.isValidRange(ipWithMask, "212.34.100.0");
+		Assert.assertFalse(notAllowed3);
+	}
+	*/
+}
diff --git a/src/test/java/org/olat/course/assessment/manager/AssessmentModeManagerTest.java b/src/test/java/org/olat/course/assessment/manager/AssessmentModeManagerTest.java
index bfeb13fa6b9..cafb42392f9 100644
--- a/src/test/java/org/olat/course/assessment/manager/AssessmentModeManagerTest.java
+++ b/src/test/java/org/olat/course/assessment/manager/AssessmentModeManagerTest.java
@@ -559,6 +559,41 @@ public class AssessmentModeManagerTest extends OlatTestCase {
 		Assert.assertTrue(assessedIdentityKeys.contains(participant2.getKey()));
 	}
 	
+	@Test
+	public void isIpAllowed_exactMatch() {
+		String ipList = "192.168.1.203";
+
+		boolean allowed1 = assessmentModeMgr.isIpAllowed(ipList, "192.168.1.203");
+		Assert.assertTrue(allowed1);
+
+		//negative test
+		boolean notAllowed1 = assessmentModeMgr.isIpAllowed(ipList, "192.168.1.129");
+		Assert.assertFalse(notAllowed1);
+		boolean notAllowed2 = assessmentModeMgr.isIpAllowed(ipList, "192.168.1.204");
+		Assert.assertFalse(notAllowed2);
+		boolean notAllowed3 = assessmentModeMgr.isIpAllowed(ipList, "192.168.100.203");
+		Assert.assertFalse(notAllowed3);
+		boolean notAllowed4 = assessmentModeMgr.isIpAllowed(ipList, "192.203.203.203");
+		Assert.assertFalse(notAllowed4);
+	}
+	
+	@Test
+	public void isIpAllowed_pseudoRange() {
+		String ipList = "192.168.1.1 - 192.168.1.128";
+
+		boolean allowed1 = assessmentModeMgr.isIpAllowed(ipList, "192.168.1.64");
+		Assert.assertTrue(allowed1);
+
+		//negative test
+		boolean notAllowed1 = assessmentModeMgr.isIpAllowed(ipList, "192.168.1.129");
+		Assert.assertFalse(notAllowed1);
+		boolean notAllowed2 = assessmentModeMgr.isIpAllowed(ipList, "192.168.1.204");
+		Assert.assertFalse(notAllowed2);
+		boolean notAllowed3 = assessmentModeMgr.isIpAllowed(ipList, "192.168.100.64");
+		Assert.assertFalse(notAllowed3);
+		boolean notAllowed4 = assessmentModeMgr.isIpAllowed(ipList, "212.203.203.64");
+		Assert.assertFalse(notAllowed4);
+	}
 
 	private AssessmentMode createMinimalAssessmentmode(RepositoryEntry entry) {
 		AssessmentMode mode = assessmentModeMgr.createAssessmentMode(entry);
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index 708b1681a77..6e782134a4a 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -62,6 +62,7 @@ import org.junit.runners.Suite;
 	org.olat.core.util.FormatterTest.class,
 	org.olat.core.util.EncoderTest.class,
 	org.olat.core.util.SimpleHtmlParserTest.class,
+	org.olat.core.util.IPUtilsTest.class,
 	org.olat.core.util.mail.manager.MailManagerTest.class,
 	org.olat.core.id.context.BusinessControlFactoryTest.class,
 	org.olat.core.id.context.HistoryManagerTest.class,
-- 
GitLab