Skip to content

Commit 400b612

Browse files
Merge pull request #543 from Heigvd/mikk-signUp
Terms of service and data policy
2 parents 3aca073 + 4f5aa9c commit 400b612

File tree

24 files changed

+2848
-2007
lines changed

24 files changed

+2848
-2007
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* The coLAB project
3+
* Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
4+
*
5+
* Licensed under the MIT License
6+
*/
7+
package ch.colabproject.colab.generator.model.annotations;
8+
9+
import java.lang.annotation.ElementType;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
14+
/**
15+
* Depict REST classes or methods which are available when user consent is not required.
16+
* <p>
17+
* If a class is annotated, all methods are impacted
18+
*
19+
*
20+
* @author mikkelvestergaard
21+
*/
22+
@Target({ElementType.METHOD})
23+
@Retention(RetentionPolicy.RUNTIME)
24+
public @interface ConsentNotRequired {
25+
26+
}

colab-api/src/main/java/ch/colabproject/colab/api/controller/user/UserManager.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ public User signup(SignUpInfo signup) {
317317
user.setFirstname(signup.getFirstname());
318318
user.setLastname(signup.getLastname());
319319
user.setAffiliation(signup.getAffiliation());
320+
user.setAgreedTime(OffsetDateTime.now());
320321

321322
validationManager.assertValid(user);
322323
validationManager.assertValid(account);
@@ -671,6 +672,17 @@ public void setLocalAccountAsVerified(LocalAccount account) {
671672
account.setVerified(Boolean.TRUE);
672673
}
673674

675+
/**
676+
* Update the user agreedTime to now
677+
*
678+
* @param userId id of the user to update
679+
*/
680+
public void updateUserAgreedTime(Long userId) {
681+
User user = assertAndGetUser(userId);
682+
OffsetDateTime now = OffsetDateTime.now();
683+
user.setAgreedTime(now);
684+
}
685+
674686
/**
675687
* Get all session linked to the current user
676688
*

colab-api/src/main/java/ch/colabproject/colab/api/model/user/User.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ public class User implements ColabEntity, WithWebsocketChannels {
104104
@JsonbTypeSerializer(DateSerDe.class)
105105
private OffsetDateTime activityDate = null;
106106

107+
/**
108+
* persisted terms and data policy agreement time
109+
*/
110+
@JsonbTypeDeserializer(DateSerDe.class)
111+
@JsonbTypeSerializer(DateSerDe.class)
112+
private OffsetDateTime agreedTime = null;
113+
107114
/**
108115
* Firstname
109116
*/
@@ -250,6 +257,20 @@ public void setActivityDate(OffsetDateTime activityDate) {
250257
this.activityDate = activityDate;
251258
}
252259

260+
/**
261+
*
262+
*
263+
* @return user agreedTime
264+
*/
265+
public OffsetDateTime getAgreedTime() { return agreedTime; }
266+
267+
/**
268+
* Set user agreedTime
269+
*
270+
* @param agreedTime new agreedTime
271+
*/
272+
public void setAgreedTime(OffsetDateTime agreedTime) { this.agreedTime = agreedTime; }
273+
253274
/**
254275
* @return user first name, may be null or empty
255276
*/
@@ -409,6 +430,7 @@ public void mergeToUpdate(ColabEntity other) throws ColabMergeException {
409430
this.setLastname(o.getLastname());
410431
this.setCommonname(o.getCommonname());
411432
this.setAffiliation(o.getAffiliation());
433+
// agreedTime cannot be changed by a simple update
412434
} else {
413435
throw new ColabMergeException(this, other);
414436
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* The coLAB project
3+
* Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
4+
*
5+
* Licensed under the MIT License
6+
*/
7+
package ch.colabproject.colab.api.rest.security;
8+
9+
import ch.colabproject.colab.api.security.TosAndDataPolicyManager;
10+
11+
import javax.inject.Inject;
12+
import javax.ws.rs.Consumes;
13+
import javax.ws.rs.GET;
14+
import javax.ws.rs.Path;
15+
import javax.ws.rs.Produces;
16+
import javax.ws.rs.core.MediaType;
17+
18+
/**
19+
* REST SecurityRestEndpoint for ToS and Data Policy
20+
*
21+
* @author mikkelvestergaard
22+
*/
23+
@Path("security")
24+
@Consumes(MediaType.APPLICATION_JSON)
25+
@Produces(MediaType.APPLICATION_JSON)
26+
public class SecurityRestEndPoint {
27+
28+
/**
29+
* To get TosAndDataPolicy timestamp
30+
*/
31+
@Inject
32+
private TosAndDataPolicyManager tosAndDataPolicyManager;
33+
34+
/**
35+
* Get the current TosAndDataPolicy as unix timestamp
36+
*
37+
* @return current TosAndDataPolicy timestamp
38+
*/
39+
@GET
40+
@Path("getTosAndDataPolicyTimeEpoch")
41+
public Long getTosAndDataPolicyTimeEpoch() { return tosAndDataPolicyManager.getEpochTime(); }
42+
}

colab-api/src/main/java/ch/colabproject/colab/api/rest/user/UserRestEndpoint.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import ch.colabproject.colab.api.persistence.jpa.user.UserDao;
2020
import ch.colabproject.colab.generator.model.annotations.AdminResource;
2121
import ch.colabproject.colab.generator.model.annotations.AuthenticationRequired;
22+
import ch.colabproject.colab.generator.model.annotations.ConsentNotRequired;
2223
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
2324
import java.util.List;
2425
import javax.inject.Inject;
@@ -238,6 +239,19 @@ public void updateUser(User user) throws ColabMergeException {
238239
userDao.updateUser(user);
239240
}
240241

242+
/**
243+
* Update user's agreedTime of given id.
244+
*
245+
* @param id id of the user who's agreedTime to update
246+
*/
247+
@POST
248+
@Path("{id : [1-9][0-9]*}/updateUserAgreedTime")
249+
@ConsentNotRequired
250+
public void updateUserAgreedTime(@PathParam("id") Long id) {
251+
logger.debug("update agreedTime to user: #{}", id);
252+
userManager.updateUserAgreedTime(id);
253+
}
254+
241255
/**
242256
* Grant admin right to user identified by given id.
243257
*

colab-api/src/main/java/ch/colabproject/colab/api/security/AuthenticationFilter.java

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
import ch.colabproject.colab.api.model.user.User;
1111
import ch.colabproject.colab.generator.model.annotations.AdminResource;
1212
import ch.colabproject.colab.generator.model.annotations.AuthenticationRequired;
13+
import ch.colabproject.colab.generator.model.annotations.ConsentNotRequired;
1314
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
15+
1416
import java.io.IOException;
1517
import java.lang.annotation.Annotation;
1618
import java.lang.reflect.Method;
@@ -24,6 +26,7 @@
2426
import javax.ws.rs.core.Context;
2527
import javax.ws.rs.ext.ExceptionMapper;
2628
import javax.ws.rs.ext.Provider;
29+
2730
import org.slf4j.Logger;
2831
import org.slf4j.LoggerFactory;
2932

@@ -68,18 +71,23 @@ public class AuthenticationFilter implements ContainerRequestFilter {
6871
@Inject
6972
private SessionManager sessionManager;
7073

74+
/**
75+
* To get TosAndDataPolicy timestamp
76+
*/
77+
@Inject
78+
private TosAndDataPolicyManager tosAndDataPolicyManager;
79+
7180
/**
7281
* Get all method or class annotations matching the given type.
7382
*
7483
* @param <T> type of annotation to search
7584
* @param annotation type of annotation to search
7685
* @param klass targeted class
7786
* @param method targeted method
78-
*
7987
* @return the list of all matching annotations found on class and method
8088
*/
8189
private <T extends Annotation> List<T> getAnnotations(Class<T> annotation,
82-
Class<?> klass, Method method) {
90+
Class<?> klass, Method method) {
8391

8492
List<T> list = new ArrayList<>();
8593

@@ -109,27 +117,36 @@ public void filter(ContainerRequestContext requestContext) throws IOException {
109117
User currentUser = requestManager.getCurrentUser();
110118
HttpErrorMessage abortWith = null;
111119

112-
if (currentUser == null) {
113-
// current user not authenticated: make sure the targeted method is accessible to
114-
// unauthenticated user
115-
List<AuthenticationRequired> annotations = getAnnotations(
120+
List<AuthenticationRequired> authAnnotations = getAnnotations(
116121
AuthenticationRequired.class,
117122
targetClass, targetMethod);
118123

119-
if (!annotations.isEmpty()) {
124+
if (!authAnnotations.isEmpty()) {
125+
if (currentUser == null) {
126+
// current user not authenticated: make sure the targeted method is accessible to
127+
// unauthenticated user
120128
// No current user but annotation required to be authenticated
121129
// abort with 401 code
122130
logger.trace("Request aborted:user is not authenticated");
123131
abortWith = HttpErrorMessage.authenticationRequired();
132+
} else {
133+
List<ConsentNotRequired> consentAnnotations = getAnnotations(
134+
ConsentNotRequired.class,
135+
targetClass, targetMethod);
136+
137+
if (consentAnnotations.isEmpty() && (currentUser.getAgreedTime() == null || currentUser.getAgreedTime().isBefore(tosAndDataPolicyManager.getTimestamp()))) {
138+
// current user is authenticated but need to accept new TosAndDataPolicy
139+
logger.trace("Request aborted:user has not agreed to new TosAndDataPolicy");
140+
abortWith = HttpErrorMessage.forbidden();
141+
}
124142
}
125-
} else {
126-
sessionManager.touchUserActivityDate();
127143
}
128144

129-
List<AdminResource> annotations = getAnnotations(
130-
AdminResource.class,
131-
targetClass, targetMethod);
132-
if (!annotations.isEmpty()) {
145+
146+
List<AdminResource> adminAnnotations = getAnnotations(
147+
AdminResource.class,
148+
targetClass, targetMethod);
149+
if (!adminAnnotations.isEmpty()) {
133150
if (currentUser == null) {
134151
// no current user : unauthorized asks for user to authenticate
135152
logger.trace("Request aborted:user is not authenticated");
@@ -145,6 +162,8 @@ public void filter(ContainerRequestContext requestContext) throws IOException {
145162

146163
if (abortWith != null) {
147164
requestContext.abortWith(exceptionMapper.toResponse(abortWith));
165+
} else {
166+
sessionManager.touchUserActivityDate();
148167
}
149168
}
150169
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* The coLAB project
3+
* Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
4+
*
5+
* Licensed under the MIT License
6+
*/
7+
package ch.colabproject.colab.api.security;
8+
9+
import java.time.Instant;
10+
import java.time.OffsetDateTime;
11+
import java.time.ZoneId;
12+
13+
/**
14+
* To store the Terms of Service and Data Policy dates
15+
*
16+
* @author mikkelvestergaard
17+
*/
18+
public class TosAndDataPolicyManager {
19+
20+
21+
/**
22+
* Epoch time of the most recent ToS and Data Policy update
23+
*/
24+
private static final Long EPOCHTIME = 1700780400L;
25+
26+
/**
27+
* Date of the most recent ToS and Data Policy update
28+
*/
29+
private static final OffsetDateTime TIMESTAMP = OffsetDateTime.ofInstant(Instant.ofEpochMilli(EPOCHTIME), ZoneId.systemDefault());
30+
31+
/**
32+
* Get ToS and Data Policy timestamp as Epoch Time
33+
*
34+
* @return the timestamp
35+
*/
36+
public Long getEpochTime() { return EPOCHTIME; }
37+
38+
/**
39+
* Get ToS and Data Policy timestamp as OffsetDateTime
40+
*
41+
* @return the timestamp
42+
*/
43+
public OffsetDateTime getTimestamp() {
44+
return TIMESTAMP;
45+
}
46+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
2+
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
3+
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
4+
xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
6+
<changeSet author="mikkelvestergaard" id="1700818809-1">
7+
<addColumn tableName="users">
8+
<column name="agreed_time" type="TIMESTAMP WITHOUT TIME ZONE"/>
9+
</addColumn>
10+
</changeSet>
11+
</databaseChangeLog>

colab-tests/src/test/java/ch/colabproject/colab/tests/rest/UserRestEndpointTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import ch.colabproject.colab.tests.ws.WebsocketClient;
2727
import java.io.IOException;
2828
import java.net.URISyntaxException;
29+
import java.time.OffsetDateTime;
2930
import java.util.Arrays;
3031
import java.util.List;
3132
import java.util.Optional;
@@ -130,6 +131,34 @@ public void testUpdateUserInternalFields() {
130131
Assertions.assertNull(client.userRestEndpoint.getCurrentUser());
131132
}
132133

134+
/**
135+
* Assert new user have agreedTime and can update it
136+
*/
137+
@Test
138+
public void testUpdateUserAgreedTime() {
139+
TestUser myUser = this.signup(
140+
"GoulashSensei",
141+
"goulashsensei@test.local",
142+
"SoSecuredPassword"
143+
);
144+
145+
this.signIn(myUser);
146+
147+
User user = client.userRestEndpoint.getCurrentUser();
148+
Assertions.assertNotNull(user);
149+
150+
OffsetDateTime userAgreedTimestampInitial = user.getAgreedTime();
151+
Assertions.assertNotNull(userAgreedTimestampInitial);
152+
153+
client.userRestEndpoint.updateUserAgreedTime(user.getId());
154+
User updatedUser = client.userRestEndpoint.getCurrentUser();
155+
156+
Assertions.assertTrue(userAgreedTimestampInitial.isBefore(updatedUser.getAgreedTime()));
157+
158+
this.signOut();
159+
Assertions.assertNull(client.userRestEndpoint.getCurrentUser());
160+
}
161+
133162
/**
134163
* Assert authenticate with wrong password fails
135164
*/

colab-webapp/default_colab.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ colab.smtp.port=1025
3636
#
3737
# Account Management Properties
3838
###############################
39-
colab.localaccount.showcreatebutton=false
39+
colab.localaccount.showcreatebutton=true
4040

4141
# Mongo DB (JCR)
4242
# docker run -d --restart always -p 27017:27017 --name colab_mongo mongo:4.4

0 commit comments

Comments
 (0)