Skip to content

Commit 8086a16

Browse files
committed
#2186 - Support per-project guest annotators
- Warn if a guest user could not be imported - Added a feature switch for guest users - Documented handling of guest users when cloning a project in user documentation - Fixed temp filename used when importing a project - Fixed issue that messages added by the importers to the import request are never shown to the user
1 parent c81d19f commit 8086a16

File tree

9 files changed

+128
-33
lines changed

9 files changed

+128
-33
lines changed

inception/inception-api-dao/src/main/java/de/tudarmstadt/ukp/clarin/webanno/api/dao/export/exporters/ProjectPermissionsExporter.java

+33-26
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static de.tudarmstadt.ukp.clarin.webanno.security.UserDao.EMPTY_PASSWORD;
2424
import static de.tudarmstadt.ukp.clarin.webanno.security.UserDao.REALM_PROJECT_PREFIX;
2525
import static de.tudarmstadt.ukp.clarin.webanno.security.model.Role.ROLE_USER;
26+
import static java.lang.String.format;
2627
import static org.apache.commons.lang3.StringUtils.startsWith;
2728

2829
import java.io.File;
@@ -125,33 +126,39 @@ public void importData(ProjectImportRequest aRequest, Project aProject,
125126
ExportedUser[] projectUsers = aExProject.getArrayProperty(KEY_USERS, ExportedUser.class);
126127
Set<String> projectUserNames = new HashSet<>();
127128
for (ExportedUser importedUser : projectUsers) {
128-
if (!userService.exists(importedUser.getUsername())) {
129-
User u = new User();
130-
u.setRealm(REALM_PROJECT_PREFIX + aProject.getId());
131-
u.setEmail(importedUser.getEmail());
132-
u.setUiName(importedUser.getUiName());
133-
u.setUsername(importedUser.getUsername());
134-
u.setCreated(importedUser.getCreated());
135-
u.setLastLogin(importedUser.getLastLogin());
136-
u.setUpdated(importedUser.getUpdated());
137-
u.setEnabled(importedUser.isEnabled());
138-
u.setRoles(Set.of(ROLE_USER));
139-
userService.create(u);
140-
141-
// Ok, this is a bug... if we export a project and then import it again into the
142-
// same instance, then the users are not created (because they exist already)
143-
// and thus the clone of the project does not have any project-bound users.
144-
// ... but ...
145-
// if we instead add users that pre-existed to this set, then we can end up adding
146-
// project-bound users from another project (i.e. from the original one which we
147-
// are cloning). That means if the original project is deleted, then the users
148-
// will be deleted and this our clone project gets its users removed. Also not good.
149-
//
150-
// So we currently stick with not importing permissions for project-bound users
151-
// from the original project... this can be fixed when/if at some point we allow
152-
// re-mapping users during import - or if we have some smart idea...
153-
projectUserNames.add(importedUser.getUsername());
129+
if (userService.exists(importedUser.getUsername())) {
130+
aRequest.addMessage(format("Unable to create project-bound user [%s] with ID "
131+
+ "[%s] because a user with this ID already exists in the system. "
132+
+ "Annotations of this user are not accessible in the imported project.",
133+
importedUser.getUiName(), importedUser.getUsername()));
134+
continue;
154135
}
136+
137+
User u = new User();
138+
u.setRealm(REALM_PROJECT_PREFIX + aProject.getId());
139+
u.setEmail(importedUser.getEmail());
140+
u.setUiName(importedUser.getUiName());
141+
u.setUsername(importedUser.getUsername());
142+
u.setCreated(importedUser.getCreated());
143+
u.setLastLogin(importedUser.getLastLogin());
144+
u.setUpdated(importedUser.getUpdated());
145+
u.setEnabled(importedUser.isEnabled());
146+
u.setRoles(Set.of(ROLE_USER));
147+
userService.create(u);
148+
149+
// Ok, this is a bug... if we export a project and then import it again into the
150+
// same instance, then the users are not created (because they exist already)
151+
// and thus the clone of the project does not have any project-bound users.
152+
// ... but ...
153+
// if we instead add users that pre-existed to this set, then we can end up adding
154+
// project-bound users from another project (i.e. from the original one which we
155+
// are cloning). That means if the original project is deleted, then the users
156+
// will be deleted and this our clone project gets its users removed. Also not good.
157+
//
158+
// So we currently stick with not importing permissions for project-bound users
159+
// from the original project... this can be fixed when/if at some point we allow
160+
// re-mapping users during import - or if we have some smart idea...
161+
projectUserNames.add(importedUser.getUsername());
155162
}
156163

157164
// Import permissions - always import permissions for the importing user and for

inception/inception-sharing/src/main/java/de/tudarmstadt/ukp/inception/sharing/AcceptInvitePage.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import de.tudarmstadt.ukp.clarin.webanno.ui.core.ApplicationSession;
6666
import de.tudarmstadt.ukp.clarin.webanno.ui.core.login.LoginProperties;
6767
import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.ProjectPageBase;
68+
import de.tudarmstadt.ukp.inception.sharing.config.InviteServiceProperties;
6869
import de.tudarmstadt.ukp.inception.sharing.model.ProjectInvite;
6970
import de.tudarmstadt.ukp.inception.ui.core.dashboard.project.ProjectDashboardPage;
7071

@@ -84,6 +85,7 @@ public class AcceptInvitePage
8485
private @SpringBean UserDao userRepository;
8586
private @SpringBean LoginProperties loginProperties;
8687
private @SpringBean SessionRegistry sessionRegistry;
88+
private @SpringBean InviteServiceProperties inviteServiceProperties;
8789

8890
private final IModel<FormData> formModel;
8991
private final IModel<ProjectInvite> invite;
@@ -110,7 +112,8 @@ public AcceptInvitePage(final PageParameters aPageParameters)
110112
.add(visibleWhen(() -> !invitationIsValid.orElse(false).getObject())));
111113

112114
formModel = new CompoundPropertyModel<>(new FormData());
113-
formModel.getObject().registeredLogin = !invite.getObject().isGuestAccessible();
115+
formModel.getObject().registeredLogin = !invite.getObject().isGuestAccessible()
116+
|| !inviteServiceProperties.isGuestsEnabled();
114117

115118
Form<FormData> form = new Form<>("acceptInvitationForm", formModel);
116119
form.add(new Label("project", PropertyModel.of(getProject(), "name")));
@@ -124,7 +127,8 @@ public AcceptInvitePage(final PageParameters aPageParameters)
124127
form.add(new LambdaAjaxButton<>("join", this::actionJoinProject));
125128
form.add(new CheckBox("registeredLogin") //
126129
.setOutputMarkupPlaceholderTag(true) //
127-
.add(visibleWhen(() -> invite.getObject().isGuestAccessible() && user == null))
130+
.add(visibleWhen(() -> invite.getObject().isGuestAccessible()
131+
&& inviteServiceProperties.isGuestsEnabled() && user == null))
128132
.add(new LambdaAjaxFormComponentUpdatingBehavior("change",
129133
_target -> _target.add(form))));
130134
form.add(new Label("invitationText", LoadableDetachableModel.of(this::getInvitationText))

inception/inception-sharing/src/main/java/de/tudarmstadt/ukp/inception/sharing/config/InviteServiceAutoConfiguration.java

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import javax.persistence.PersistenceContext;
2222

2323
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
24+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2425
import org.springframework.context.annotation.Bean;
2526
import org.springframework.context.annotation.Configuration;
2627
import org.springframework.core.annotation.Order;
@@ -32,6 +33,7 @@
3233
import de.tudarmstadt.ukp.inception.sharing.project.ProjectSharingMenuItem;
3334

3435
@Configuration
36+
@EnableConfigurationProperties(InviteServicePropertiesImpl.class)
3537
@ConditionalOnProperty(prefix = "sharing.invites", name = "enabled", havingValue = "true")
3638
public class InviteServiceAutoConfiguration
3739
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Licensed to the Technische Universität Darmstadt under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The Technische Universität Darmstadt
6+
* licenses this file to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License.
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package de.tudarmstadt.ukp.inception.sharing.config;
19+
20+
public interface InviteServiceProperties
21+
{
22+
boolean isGuestsEnabled();
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Licensed to the Technische Universität Darmstadt under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The Technische Universität Darmstadt
6+
* licenses this file to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License.
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package de.tudarmstadt.ukp.inception.sharing.config;
19+
20+
import org.springframework.boot.context.properties.ConfigurationProperties;
21+
22+
@ConfigurationProperties("sharing")
23+
public class InviteServicePropertiesImpl
24+
implements InviteServiceProperties
25+
{
26+
private boolean guestsEnabled;
27+
28+
public boolean isGuestsEnabled()
29+
{
30+
return guestsEnabled;
31+
}
32+
33+
public void setGuestsEnabled(boolean aGuestsEnabled)
34+
{
35+
guestsEnabled = aGuestsEnabled;
36+
}
37+
}

inception/inception-sharing/src/main/java/de/tudarmstadt/ukp/inception/sharing/project/InviteProjectSettingsPanel.java

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import de.tudarmstadt.ukp.clarin.webanno.ui.core.settings.ProjectSettingsPanelBase;
5252
import de.tudarmstadt.ukp.inception.sharing.AcceptInvitePage;
5353
import de.tudarmstadt.ukp.inception.sharing.InviteService;
54+
import de.tudarmstadt.ukp.inception.sharing.config.InviteServiceProperties;
5455
import de.tudarmstadt.ukp.inception.sharing.model.ProjectInvite;
5556

5657
public class InviteProjectSettingsPanel
@@ -60,6 +61,7 @@ public class InviteProjectSettingsPanel
6061

6162
private @SpringBean InviteService inviteService;
6263
private @SpringBean ServletContext servletContext;
64+
private @SpringBean InviteServiceProperties inviteServiceProperties;
6365

6466
private IModel<ProjectInvite> invite;
6567

@@ -94,6 +96,7 @@ protected void onDisabled(ComponentTag tag)
9496
detailsForm.add(new TextArea<>("invitationText").add(AttributeModifier
9597
.replace("placeholder", new ResourceModel("invitationText.placeholder"))));
9698
detailsForm.add(new CheckBox("guestAccessible").setOutputMarkupId(true)
99+
.add(visibleWhen(() -> inviteServiceProperties.isGuestsEnabled()))
97100
.add(new LambdaAjaxFormSubmittingBehavior("change", _target -> _target.add(this))));
98101
detailsForm.add(new TextField<>("userIdPlaceholder")
99102
.add(visibleWhen(invite.map(ProjectInvite::isGuestAccessible))));

inception/inception-sharing/src/main/resources/META-INF/asciidoc/admin-guide/settings_sharing.adoc

+5
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,10 @@ instances.
4242
| enable/disable invite links
4343
| false
4444
| true
45+
46+
| sharing.invites.guests-enabled
47+
| enable/disable guest annotators
48+
| false
49+
| true
4550
|===
4651

inception/inception-sharing/src/main/resources/META-INF/asciidoc/user-guide/projects_sharing.adoc

+11-1
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,20 @@ image::sharing_settings.png[align="center"]
2323

2424
The user can now follow the invite link by entering it into a browser. She might be prompted to log into {product-name} and is then automatically added to the project with annotator rights and directed to the project dashboard page. She can now start annotating.
2525

26+
NOTE: This feature is not enabled by default. However, you can enable it by adding `sharing.invites.enabled=true` to the `settings.properties` file (see the <<admin-guide.adoc#sect_settings, Admin Guide>>).
27+
2628
By default, users need to already have a {product-name} account to be able to use the link. However,
2729
by activating the option *Allow guest annotators*, a person accessing the invite link can simply
2830
enter any user ID they like and access the project using that ID. This ID is then valid only via the
2931
invite link and only for the particular project. The ID is not protected by a password. When the
3032
manager removes the project, the internal accounts backing the ID are automatically removed as well.
3133

32-
NOTE: This feature is not enabled by default. However, you can enable it by adding `sharing.invites.enabled=true` to the `settings.properties` file (see the <<admin-guide.adoc#sect_settings, Admin Guide>>).
34+
NOTE: When importing a project with guest annotators, the annotations of the guests can only be
35+
imported if the respective guest accounts do not yet exist in the {product-name} instance. This
36+
means, it is possible to make a backup of a project and to import it into another {product-name}
37+
instance or also into the original instance after deleting the original project. However, when
38+
importing a project as a clone of an existing project in the same instance, the imported project
39+
will not have any guest annotators.
40+
41+
NOTE: This feature is not enabled by default. However, you can enable it by adding `sharing.invites.guests-enabled=true` to the `settings.properties` file (see the <<admin-guide.adoc#sect_settings, Admin Guide>>).
42+

inception/inception-ui-project/src/main/java/de/tudarmstadt/ukp/clarin/webanno/ui/project/ProjectImportPanel.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,12 @@ else if (currentUserIsProjectCreator) {
158158

159159
List<Project> importedProjects = new ArrayList<>();
160160
for (FileUpload exportedProject : exportedProjects) {
161+
ProjectImportRequest request = new ProjectImportRequest(createMissingUsers,
162+
importPermissions, manager);
163+
161164
try {
162165
// Workaround for WICKET-6425
163-
File tempFile = File.createTempFile("webanno-training", null);
166+
File tempFile = File.createTempFile("project-import", null);
164167
try (InputStream is = new BufferedInputStream(exportedProject.getInputStream());
165168
OutputStream os = new FileOutputStream(tempFile);) {
166169
if (!ZipUtils.isZipStream(is)) {
@@ -172,22 +175,23 @@ else if (currentUserIsProjectCreator) {
172175
throw new IOException("ZIP file is not a WebAnno project archive");
173176
}
174177

175-
ProjectImportRequest request = new ProjectImportRequest(createMissingUsers,
176-
importPermissions, manager);
177178
importedProjects
178179
.add(exportService.importProject(request, new ZipFile(tempFile)));
179180
}
180181
finally {
181182
tempFile.delete();
183+
184+
request.getMessages().forEach(m -> getSession().warn(m));
182185
}
183186
}
184187
catch (Exception e) {
185-
aTarget.addChildren(getPage(), IFeedback.class);
186188
error("Error importing project: " + ExceptionUtils.getRootCauseMessage(e));
187189
LOG.error("Error importing project", e);
188190
}
189191
}
190192

193+
aTarget.addChildren(getPage(), IFeedback.class);
194+
191195
if (!importedProjects.isEmpty() && selectedModel != null) {
192196
selectedModel.setObject(importedProjects.get(importedProjects.size() - 1));
193197
}

0 commit comments

Comments
 (0)