Skip to content

Commit

Permalink
#2186 - Support per-project guest annotators
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
reckart committed Apr 17, 2021
1 parent c81d19f commit 8086a16
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static de.tudarmstadt.ukp.clarin.webanno.security.UserDao.EMPTY_PASSWORD;
import static de.tudarmstadt.ukp.clarin.webanno.security.UserDao.REALM_PROJECT_PREFIX;
import static de.tudarmstadt.ukp.clarin.webanno.security.model.Role.ROLE_USER;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.startsWith;

import java.io.File;
Expand Down Expand Up @@ -125,33 +126,39 @@ public void importData(ProjectImportRequest aRequest, Project aProject,
ExportedUser[] projectUsers = aExProject.getArrayProperty(KEY_USERS, ExportedUser.class);
Set<String> projectUserNames = new HashSet<>();
for (ExportedUser importedUser : projectUsers) {
if (!userService.exists(importedUser.getUsername())) {
User u = new User();
u.setRealm(REALM_PROJECT_PREFIX + aProject.getId());
u.setEmail(importedUser.getEmail());
u.setUiName(importedUser.getUiName());
u.setUsername(importedUser.getUsername());
u.setCreated(importedUser.getCreated());
u.setLastLogin(importedUser.getLastLogin());
u.setUpdated(importedUser.getUpdated());
u.setEnabled(importedUser.isEnabled());
u.setRoles(Set.of(ROLE_USER));
userService.create(u);

// Ok, this is a bug... if we export a project and then import it again into the
// same instance, then the users are not created (because they exist already)
// and thus the clone of the project does not have any project-bound users.
// ... but ...
// if we instead add users that pre-existed to this set, then we can end up adding
// project-bound users from another project (i.e. from the original one which we
// are cloning). That means if the original project is deleted, then the users
// will be deleted and this our clone project gets its users removed. Also not good.
//
// So we currently stick with not importing permissions for project-bound users
// from the original project... this can be fixed when/if at some point we allow
// re-mapping users during import - or if we have some smart idea...
projectUserNames.add(importedUser.getUsername());
if (userService.exists(importedUser.getUsername())) {
aRequest.addMessage(format("Unable to create project-bound user [%s] with ID "
+ "[%s] because a user with this ID already exists in the system. "
+ "Annotations of this user are not accessible in the imported project.",
importedUser.getUiName(), importedUser.getUsername()));
continue;
}

User u = new User();
u.setRealm(REALM_PROJECT_PREFIX + aProject.getId());
u.setEmail(importedUser.getEmail());
u.setUiName(importedUser.getUiName());
u.setUsername(importedUser.getUsername());
u.setCreated(importedUser.getCreated());
u.setLastLogin(importedUser.getLastLogin());
u.setUpdated(importedUser.getUpdated());
u.setEnabled(importedUser.isEnabled());
u.setRoles(Set.of(ROLE_USER));
userService.create(u);

// Ok, this is a bug... if we export a project and then import it again into the
// same instance, then the users are not created (because they exist already)
// and thus the clone of the project does not have any project-bound users.
// ... but ...
// if we instead add users that pre-existed to this set, then we can end up adding
// project-bound users from another project (i.e. from the original one which we
// are cloning). That means if the original project is deleted, then the users
// will be deleted and this our clone project gets its users removed. Also not good.
//
// So we currently stick with not importing permissions for project-bound users
// from the original project... this can be fixed when/if at some point we allow
// re-mapping users during import - or if we have some smart idea...
projectUserNames.add(importedUser.getUsername());
}

// Import permissions - always import permissions for the importing user and for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import de.tudarmstadt.ukp.clarin.webanno.ui.core.ApplicationSession;
import de.tudarmstadt.ukp.clarin.webanno.ui.core.login.LoginProperties;
import de.tudarmstadt.ukp.clarin.webanno.ui.core.page.ProjectPageBase;
import de.tudarmstadt.ukp.inception.sharing.config.InviteServiceProperties;
import de.tudarmstadt.ukp.inception.sharing.model.ProjectInvite;
import de.tudarmstadt.ukp.inception.ui.core.dashboard.project.ProjectDashboardPage;

Expand All @@ -84,6 +85,7 @@ public class AcceptInvitePage
private @SpringBean UserDao userRepository;
private @SpringBean LoginProperties loginProperties;
private @SpringBean SessionRegistry sessionRegistry;
private @SpringBean InviteServiceProperties inviteServiceProperties;

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

formModel = new CompoundPropertyModel<>(new FormData());
formModel.getObject().registeredLogin = !invite.getObject().isGuestAccessible();
formModel.getObject().registeredLogin = !invite.getObject().isGuestAccessible()
|| !inviteServiceProperties.isGuestsEnabled();

Form<FormData> form = new Form<>("acceptInvitationForm", formModel);
form.add(new Label("project", PropertyModel.of(getProject(), "name")));
Expand All @@ -124,7 +127,8 @@ public AcceptInvitePage(final PageParameters aPageParameters)
form.add(new LambdaAjaxButton<>("join", this::actionJoinProject));
form.add(new CheckBox("registeredLogin") //
.setOutputMarkupPlaceholderTag(true) //
.add(visibleWhen(() -> invite.getObject().isGuestAccessible() && user == null))
.add(visibleWhen(() -> invite.getObject().isGuestAccessible()
&& inviteServiceProperties.isGuestsEnabled() && user == null))
.add(new LambdaAjaxFormComponentUpdatingBehavior("change",
_target -> _target.add(form))));
form.add(new Label("invitationText", LoadableDetachableModel.of(this::getInvitationText))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import javax.persistence.PersistenceContext;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
Expand All @@ -32,6 +33,7 @@
import de.tudarmstadt.ukp.inception.sharing.project.ProjectSharingMenuItem;

@Configuration
@EnableConfigurationProperties(InviteServicePropertiesImpl.class)
@ConditionalOnProperty(prefix = "sharing.invites", name = "enabled", havingValue = "true")
public class InviteServiceAutoConfiguration
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Licensed to the Technische Universität Darmstadt under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The Technische Universität Darmstadt
* licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.tudarmstadt.ukp.inception.sharing.config;

public interface InviteServiceProperties
{
boolean isGuestsEnabled();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Licensed to the Technische Universität Darmstadt under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The Technische Universität Darmstadt
* licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.tudarmstadt.ukp.inception.sharing.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("sharing")
public class InviteServicePropertiesImpl
implements InviteServiceProperties
{
private boolean guestsEnabled;

public boolean isGuestsEnabled()
{
return guestsEnabled;
}

public void setGuestsEnabled(boolean aGuestsEnabled)
{
guestsEnabled = aGuestsEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import de.tudarmstadt.ukp.clarin.webanno.ui.core.settings.ProjectSettingsPanelBase;
import de.tudarmstadt.ukp.inception.sharing.AcceptInvitePage;
import de.tudarmstadt.ukp.inception.sharing.InviteService;
import de.tudarmstadt.ukp.inception.sharing.config.InviteServiceProperties;
import de.tudarmstadt.ukp.inception.sharing.model.ProjectInvite;

public class InviteProjectSettingsPanel
Expand All @@ -60,6 +61,7 @@ public class InviteProjectSettingsPanel

private @SpringBean InviteService inviteService;
private @SpringBean ServletContext servletContext;
private @SpringBean InviteServiceProperties inviteServiceProperties;

private IModel<ProjectInvite> invite;

Expand Down Expand Up @@ -94,6 +96,7 @@ protected void onDisabled(ComponentTag tag)
detailsForm.add(new TextArea<>("invitationText").add(AttributeModifier
.replace("placeholder", new ResourceModel("invitationText.placeholder"))));
detailsForm.add(new CheckBox("guestAccessible").setOutputMarkupId(true)
.add(visibleWhen(() -> inviteServiceProperties.isGuestsEnabled()))
.add(new LambdaAjaxFormSubmittingBehavior("change", _target -> _target.add(this))));
detailsForm.add(new TextField<>("userIdPlaceholder")
.add(visibleWhen(invite.map(ProjectInvite::isGuestAccessible))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,10 @@ instances.
| enable/disable invite links
| false
| true

| sharing.invites.guests-enabled
| enable/disable guest annotators
| false
| true
|===

Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,20 @@ image::sharing_settings.png[align="center"]

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.

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>>).

By default, users need to already have a {product-name} account to be able to use the link. However,
by activating the option *Allow guest annotators*, a person accessing the invite link can simply
enter any user ID they like and access the project using that ID. This ID is then valid only via the
invite link and only for the particular project. The ID is not protected by a password. When the
manager removes the project, the internal accounts backing the ID are automatically removed as well.

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>>).
NOTE: When importing a project with guest annotators, the annotations of the guests can only be
imported if the respective guest accounts do not yet exist in the {product-name} instance. This
means, it is possible to make a backup of a project and to import it into another {product-name}
instance or also into the original instance after deleting the original project. However, when
importing a project as a clone of an existing project in the same instance, the imported project
will not have any guest annotators.

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>>).

Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,12 @@ else if (currentUserIsProjectCreator) {

List<Project> importedProjects = new ArrayList<>();
for (FileUpload exportedProject : exportedProjects) {
ProjectImportRequest request = new ProjectImportRequest(createMissingUsers,
importPermissions, manager);

try {
// Workaround for WICKET-6425
File tempFile = File.createTempFile("webanno-training", null);
File tempFile = File.createTempFile("project-import", null);
try (InputStream is = new BufferedInputStream(exportedProject.getInputStream());
OutputStream os = new FileOutputStream(tempFile);) {
if (!ZipUtils.isZipStream(is)) {
Expand All @@ -172,22 +175,23 @@ else if (currentUserIsProjectCreator) {
throw new IOException("ZIP file is not a WebAnno project archive");
}

ProjectImportRequest request = new ProjectImportRequest(createMissingUsers,
importPermissions, manager);
importedProjects
.add(exportService.importProject(request, new ZipFile(tempFile)));
}
finally {
tempFile.delete();

request.getMessages().forEach(m -> getSession().warn(m));
}
}
catch (Exception e) {
aTarget.addChildren(getPage(), IFeedback.class);
error("Error importing project: " + ExceptionUtils.getRootCauseMessage(e));
LOG.error("Error importing project", e);
}
}

aTarget.addChildren(getPage(), IFeedback.class);

if (!importedProjects.isEmpty() && selectedModel != null) {
selectedModel.setObject(importedProjects.get(importedProjects.size() - 1));
}
Expand Down

0 comments on commit 8086a16

Please sign in to comment.