Skip to content

Commit c9ff812

Browse files
committed
Closes #2478: API to download CSV file with all registered users
1 parent 0e5c4e4 commit c9ff812

File tree

3 files changed

+292
-0
lines changed

3 files changed

+292
-0
lines changed

dataverse-webapp/src/main/java/edu/harvard/iq/dataverse/api/Users.java

+26
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22

33
import edu.harvard.iq.dataverse.api.annotations.ApiWriteOperation;
44
import edu.harvard.iq.dataverse.persistence.user.AuthenticatedUser;
5+
import edu.harvard.iq.dataverse.userdata.AuthenticatedUserCsvWriter;
56
import edu.harvard.iq.dataverse.users.ChangeUserIdentifierService;
67
import edu.harvard.iq.dataverse.users.MergeInAccountService;
78

89
import javax.ejb.EJBException;
910
import javax.inject.Inject;
11+
import javax.ws.rs.GET;
1012
import javax.ws.rs.POST;
1113
import javax.ws.rs.Path;
1214
import javax.ws.rs.PathParam;
15+
import javax.ws.rs.Produces;
1316
import javax.ws.rs.core.Response;
17+
import javax.ws.rs.core.StreamingOutput;
1418

1519
/**
1620
*
@@ -25,6 +29,28 @@ public class Users extends AbstractApiBean {
2529
@Inject
2630
private MergeInAccountService mergeInAccountService;
2731

32+
@Inject
33+
private AuthenticatedUserCsvWriter authenticatedUserCsvWriter;
34+
35+
@GET
36+
@Produces({"text/csv"})
37+
public Response listUsersCSV() {
38+
try {
39+
AuthenticatedUser user = findAuthenticatedUserOrDie();
40+
if (!user.isSuperuser()) {
41+
return error(Response.Status.FORBIDDEN, "Superusers only.");
42+
}
43+
} catch (WrappedResponse ex) {
44+
return error(Response.Status.FORBIDDEN, "Superusers only.");
45+
}
46+
47+
StreamingOutput csvContent = output -> authenticatedUserCsvWriter.write(output, authSvc.findAllAuthenticatedUsers());
48+
49+
return Response.ok(csvContent)
50+
.header("Content-Disposition", "attachment; filename=\"authenticated-users.csv\"")
51+
.build();
52+
}
53+
2854
@POST
2955
@ApiWriteOperation
3056
@Path("{consumedIdentifier}/mergeIntoUser/{baseIdentifier}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package edu.harvard.iq.dataverse.userdata;
2+
3+
import edu.harvard.iq.dataverse.common.Util;
4+
import edu.harvard.iq.dataverse.persistence.user.AuthenticatedUser;
5+
import edu.harvard.iq.dataverse.persistence.user.AuthenticatedUserLookup;
6+
import io.vavr.control.Option;
7+
import org.apache.commons.csv.CSVFormat;
8+
import org.apache.commons.csv.CSVPrinter;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
import javax.ejb.Stateless;
13+
import java.io.BufferedWriter;
14+
import java.io.IOException;
15+
import java.io.OutputStream;
16+
import java.io.OutputStreamWriter;
17+
import java.io.Writer;
18+
import java.sql.Timestamp;
19+
import java.util.Arrays;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Locale;
23+
import java.util.Map;
24+
import java.util.stream.Collectors;
25+
26+
/**
27+
* Writes a CSV file with authenticated user details.
28+
*/
29+
@Stateless
30+
public class AuthenticatedUserCsvWriter {
31+
32+
private static final Logger logger = LoggerFactory.getLogger(AuthenticatedUserCsvWriter.class);
33+
34+
// -------------------- LOGIC --------------------
35+
36+
public void write(OutputStream outputStream, List<AuthenticatedUser> authenticatedUsers) throws IOException {
37+
try (Writer writer = new OutputStreamWriter(outputStream);
38+
BufferedWriter streamWriter = new BufferedWriter(writer);
39+
CSVPrinter csvPrinter = new CSVPrinter(streamWriter, CSVFormat.DEFAULT)) {
40+
41+
csvPrinter.printRecord(AuthenticatedUserCSVRecord.getHeaders());
42+
for(AuthenticatedUser user : authenticatedUsers) {
43+
csvPrinter.printRecord(buildRecord(user).getValues());
44+
}
45+
} catch (IOException ioe) {
46+
logger.error("Couldn't write user data to csv", ioe);
47+
throw ioe;
48+
}
49+
}
50+
51+
// -------------------- PRIVATE --------------------
52+
53+
private AuthenticatedUserCSVRecord buildRecord(AuthenticatedUser user) {
54+
AuthenticatedUserCSVRecord record = new AuthenticatedUserCSVRecord();
55+
56+
record.setId(user.getId());
57+
record.setUsername(user.getIdentifier());
58+
record.setName(user.getDisplayInfo().getTitle());
59+
record.setEmail(user.getEmail());
60+
record.setAffiliation(user.getAffiliation());
61+
record.setSuperuser(user.isSuperuser());
62+
record.setAuthentication(user.getAuthenticatedUserLookup());
63+
record.setVerificationStatus(user.getEmailConfirmed());
64+
record.setNotificationLanguage(user.getNotificationsLanguage());
65+
record.setAccountCreation(user.getCreatedTime());
66+
record.setLastLogin(user.getLastLoginTime());
67+
record.setLastApiUse(user.getLastApiUseTime());
68+
return record;
69+
}
70+
71+
// -------------------- INNER CLASSES --------------------
72+
73+
enum AuthenticatedUserCsvColumn {
74+
ID("ID"),
75+
USERNAME("Username"),
76+
NAME("Name"),
77+
EMAIL("Email"),
78+
AFFILIATION("Affiliation"),
79+
SUPERUSER("Superuser"),
80+
AUTHENTICATION("Authentication"),
81+
VERIFICATION_STATUS("Verification status"),
82+
NOTIFICATION_LANGUAGE("Notification language"),
83+
ACCOUNT_CREATION("Account creation"),
84+
LAST_LOGIN("Last login"),
85+
LAST_API_USE("Last API use");
86+
87+
final String columnName;
88+
89+
AuthenticatedUserCsvColumn(String columnName) {
90+
this.columnName = columnName;
91+
}
92+
93+
public String getColumnName() {
94+
return columnName;
95+
}
96+
}
97+
98+
static class AuthenticatedUserCSVRecord {
99+
100+
private static final List<String> CSV_HEADERS = Arrays.stream(AuthenticatedUserCsvColumn.values())
101+
.map(AuthenticatedUserCsvColumn::getColumnName)
102+
.collect(Collectors.toList());
103+
104+
private Map<AuthenticatedUserCsvColumn, String> data = new HashMap<>();
105+
106+
public static List<String> getHeaders() {
107+
return CSV_HEADERS;
108+
}
109+
110+
public List<String> getValues() {
111+
return Arrays.stream(AuthenticatedUserCsvColumn.values())
112+
.map(data::get)
113+
.collect(Collectors.toList());
114+
}
115+
116+
public void setId(Long id) {
117+
data.put(AuthenticatedUserCsvColumn.ID, safeToString(id));
118+
}
119+
120+
public void setUsername(String username) {
121+
data.put(AuthenticatedUserCsvColumn.USERNAME, safeToString(username));
122+
}
123+
124+
public void setName(String name) {
125+
data.put(AuthenticatedUserCsvColumn.NAME, safeToString(name));
126+
}
127+
128+
public void setEmail(String email) {
129+
data.put(AuthenticatedUserCsvColumn.EMAIL, safeToString(email));
130+
}
131+
132+
public void setAffiliation(String affiliation) {
133+
data.put(AuthenticatedUserCsvColumn.AFFILIATION, safeToString(affiliation));
134+
}
135+
136+
public void setSuperuser(boolean superuser) {
137+
data.put(AuthenticatedUserCsvColumn.SUPERUSER, Boolean.toString(superuser));
138+
}
139+
140+
public void setAuthentication(AuthenticatedUserLookup lookup) {
141+
data.put(AuthenticatedUserCsvColumn.AUTHENTICATION, Option.of(lookup)
142+
.map(AuthenticatedUserLookup::getAuthenticationProviderId).getOrElse(""));
143+
}
144+
145+
public void setVerificationStatus(Timestamp emailConfirmed) {
146+
data.put(AuthenticatedUserCsvColumn.VERIFICATION_STATUS, Option.of(emailConfirmed)
147+
.map(s -> "Verified").getOrElse("Not verified"));
148+
}
149+
150+
public void setNotificationLanguage(Locale notificationsLanguage) {
151+
data.put(AuthenticatedUserCsvColumn.NOTIFICATION_LANGUAGE, Option.of(notificationsLanguage)
152+
.map(Locale::getLanguage).getOrElse(""));
153+
}
154+
155+
public void setAccountCreation(Timestamp createdTime) {
156+
data.put(AuthenticatedUserCsvColumn.ACCOUNT_CREATION, safeTimestampToString(createdTime));
157+
}
158+
159+
public void setLastLogin(Timestamp lastLoginTime) {
160+
data.put(AuthenticatedUserCsvColumn.LAST_LOGIN, safeTimestampToString(lastLoginTime));
161+
}
162+
163+
public void setLastApiUse(Timestamp lastApiUseTime) {
164+
data.put(AuthenticatedUserCsvColumn.LAST_API_USE, safeTimestampToString(lastApiUseTime));
165+
}
166+
167+
// -------------------- PRIVATE --------------------
168+
169+
private String safeTimestampToString(Object object) {
170+
return object != null ? Util.getDateTimeFormat().format(object) : null;
171+
}
172+
173+
private String safeToString(Object object) {
174+
return object != null ? object.toString() : "";
175+
}
176+
}
177+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package edu.harvard.iq.dataverse.userdata;
2+
3+
import com.google.common.collect.Lists;
4+
import edu.harvard.iq.dataverse.persistence.MocksFactory;
5+
import edu.harvard.iq.dataverse.persistence.user.AuthenticatedUser;
6+
import edu.harvard.iq.dataverse.persistence.user.AuthenticatedUserLookup;
7+
import io.vavr.control.Option;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.io.ByteArrayOutputStream;
11+
import java.io.IOException;
12+
import java.sql.Timestamp;
13+
import java.util.Arrays;
14+
import java.util.Collections;
15+
import java.util.List;
16+
import java.util.Locale;
17+
import java.util.function.Consumer;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
21+
public class AuthenticatedUserCsvWriterTest {
22+
23+
private AuthenticatedUserCsvWriter csvWriter = new AuthenticatedUserCsvWriter();
24+
25+
// -------------------- TESTS --------------------
26+
27+
@Test
28+
public void write() throws IOException {
29+
// given
30+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
31+
List<AuthenticatedUser> users = Lists.newArrayList(
32+
defaultUser(1, "jo", "John", "Doe"),
33+
defaultUser(2, "ma", "Mary", "Sue",
34+
u -> u.setLastLoginTime(null), u -> u.setLastApiUseTime(null)),
35+
defaultUser(3, "df", "Diana", "Flores",
36+
u -> u.setSuperuser(true), u -> u.setNotificationsLanguage(Locale.FRENCH), u -> u.setAffiliation(null)),
37+
defaultUser(4, "ba", "Barb", "Wire",
38+
u -> u.setEmailConfirmed(null), u -> u.setAffiliation(null), u -> u.getAuthenticatedUserLookup().setAuthenticationProviderId("saml")),
39+
defaultUser(5, "", "Mr.", "special\",\"char.txt",
40+
u -> u.setCreatedTime(null), u -> u.setLastApiUseTime(null), u -> u.setLastLoginTime(null), u -> u.setEmail(""),
41+
u -> u.setAffiliation(null), u -> u.setEmailConfirmed(null), u -> u.setNotificationsLanguage(null))
42+
);
43+
44+
// when
45+
csvWriter.write(outputStream, users);
46+
47+
// then
48+
String[] csv = outputStream.toString().split("\r\n");
49+
assertEquals(6, csv.length);
50+
assertEquals("ID,Username,Name,Email,Affiliation,Superuser,Authentication,Verification status,Notification language,Account creation,Last login,Last API use", csv[0]);
51+
assertEquals("1,@jo,John Doe,[email protected],UnitTester,false,buildIn,Verified,en,2024-05-28T01:01:01Z,2024-05-28T02:02:02Z,2024-05-28T03:03:03Z", csv[1]);
52+
assertEquals("2,@ma,Mary Sue,[email protected],UnitTester,false,buildIn,Verified,en,2024-05-28T01:01:01Z,,", csv[2]);
53+
assertEquals("3,@df,Diana Flores,[email protected],,true,buildIn,Verified,fr,2024-05-28T01:01:01Z,2024-05-28T02:02:02Z,2024-05-28T03:03:03Z", csv[3]);
54+
assertEquals("4,@ba,Barb Wire,[email protected],,false,saml,Not verified,en,2024-05-28T01:01:01Z,2024-05-28T02:02:02Z,2024-05-28T03:03:03Z", csv[4]);
55+
assertEquals("5,@,\"Mr. special\"\",\"\"char.txt\",,,false,buildIn,Not verified,,,,", csv[5]);
56+
}
57+
58+
@Test
59+
public void write__empty_list() throws IOException {
60+
// when
61+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
62+
csvWriter.write(outputStream, Collections.emptyList());
63+
64+
// then
65+
String[] csv = outputStream.toString().split("\r\n");
66+
assertEquals(1, csv.length);
67+
assertEquals("ID,Username,Name,Email,Affiliation,Superuser,Authentication,Verification status,Notification language,Account creation,Last login,Last API use", csv[0]);
68+
}
69+
70+
// -------------------- PRIVATE --------------------
71+
72+
@SafeVarargs
73+
private static AuthenticatedUser defaultUser(int id, String username, String firstName, String lastname, Consumer<AuthenticatedUser>... overrides) {
74+
AuthenticatedUser user = MocksFactory.makeAuthenticatedUser(firstName, lastname);
75+
user.setId((long) id);
76+
user.setUserIdentifier(username);
77+
user.setAuthenticatedUserLookup(new AuthenticatedUserLookup());
78+
user.getAuthenticatedUserLookup().setAuthenticationProviderId("buildIn");
79+
user.getAuthenticatedUserLookup().setPersistentUserId(String.valueOf(id));
80+
user.setCreatedTime(Timestamp.valueOf("2024-05-28 01:01:01"));
81+
user.setLastLoginTime(Timestamp.valueOf("2024-05-28 02:02:02"));
82+
user.setLastApiUseTime(Timestamp.valueOf("2024-05-28 03:03:03"));
83+
user.setEmailConfirmed(Timestamp.valueOf("2024-05-28 04:04:04"));
84+
85+
Option.of(overrides).forEach(o -> Arrays.stream(o).forEach(override -> override.accept(user)));
86+
87+
return user;
88+
}
89+
}

0 commit comments

Comments
 (0)