package edu.harvard.iq.dataverse.api; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.EMailValidator; import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord; import edu.harvard.iq.dataverse.api.dto.RoleDTO; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.UserIdentifier; import edu.harvard.iq.dataverse.authorization.exceptions.AuthenticationProviderFactoryNotFoundException; import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationSetupException; import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderRow; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibServiceBean; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUtil; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailData; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailException; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailInitResponse; import edu.harvard.iq.dataverse.engine.command.impl.PublishDataverseCommand; import edu.harvard.iq.dataverse.settings.Setting; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonObjectBuilder; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; import java.io.StringReader; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.EJB; import javax.ejb.Stateless; import javax.json.JsonObject; import javax.json.JsonReader; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.ws.rs.Produces; import javax.ws.rs.core.Response.Status; import static edu.harvard.iq.dataverse.api.AbstractApiBean.error; import edu.harvard.iq.dataverse.authorization.RoleAssignee; import edu.harvard.iq.dataverse.authorization.users.User; /** * Where the secure, setup API calls live. * @author michael */ @Stateless @Path("admin") public class Admin extends AbstractApiBean { private static final Logger logger = Logger.getLogger(Admin.class.getName()); @EJB BuiltinUserServiceBean builtinUserService; @EJB ShibServiceBean shibService; @Path("settings") @GET public Response listAllSettings() { JsonObjectBuilder bld = jsonObjectBuilder(); settingsSvc.listAll().forEach( s -> bld.add(s.getName(), s.getContent())); return ok(bld); } @Path("settings/{name}") @PUT public Response putSetting( @PathParam("name") String name, String content ) { Setting s = settingsSvc.set(name, content); return ok( jsonObjectBuilder().add(s.getName(), s.getContent()) ); } @Path("settings/{name}") @GET public Response getSetting( @PathParam("name") String name ) { String s = settingsSvc.get(name); return ( s != null ) ? ok( s ) : notFound("Setting " + name + " not found"); } @Path("settings/{name}") @DELETE public Response deleteSetting( @PathParam("name") String name ) { settingsSvc.delete(name); return ok("Setting " + name + " deleted."); } @Path("authenticationProviderFactories") @GET public Response listAuthProviderFactories() { return ok(authSvc.listProviderFactories() .stream() .map( f -> jsonObjectBuilder() .add("alias", f.getAlias() ) .add("info", f.getInfo() ) ) .collect( toJsonArray() ) ); } @Path("authenticationProviders") @GET public Response listAuthProviders() { return ok(em.createNamedQuery("AuthenticationProviderRow.findAll", AuthenticationProviderRow.class).getResultList() .stream().map( r->json(r) ).collect( toJsonArray() )); } @Path("authenticationProviders") @POST public Response addProvider( AuthenticationProviderRow row ) { try { AuthenticationProviderRow managed = em.find(AuthenticationProviderRow.class,row.getId()); if ( managed != null ) { managed = em.merge(row); } else { em.persist(row); managed = row; } if ( managed.isEnabled() ) { AuthenticationProvider provider = authSvc.loadProvider(managed); authSvc.deregisterProvider(provider.getId()); authSvc.registerProvider(provider); } return created("/s/authenticationProviders/"+managed.getId(), json(managed)); } catch ( AuthorizationSetupException e ) { return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage() ); } } @Path("authenticationProviders/{id}") @GET public Response showProvider( @PathParam("id") String id ) { AuthenticationProviderRow row = em.find(AuthenticationProviderRow.class, id); return (row != null ) ? ok( json(row) ) : error(Status.NOT_FOUND,"Can't find authetication provider with id '" + id + "'"); } @POST @Path("authenticationProviders/{id}/:enabled") @Produces("application/json") public Response enableAuthenticationProvider( @PathParam("id")String id, String body ) { if ( ! Util.isBoolean(body) ) { return error(Response.Status.BAD_REQUEST, "Illegal value '" + body + "'. Use 'true' or 'false'"); } boolean enable = Util.isTrue(body); AuthenticationProviderRow row = em.find(AuthenticationProviderRow.class, id); if ( row == null ) { return notFound("Can't find authentication provider with id '" + id + "'"); } row.setEnabled(enable); em.merge(row); if ( enable ) { // enable a provider if ( authSvc.getAuthenticationProvider(id) != null ) { return ok( String.format("Authentication provider '%s' already enabled", id)); } try { authSvc.registerProvider( authSvc.loadProvider(row) ); return ok(String.format("Authentication Provider %s enabled", row.getId())); } catch (AuthenticationProviderFactoryNotFoundException ex) { return notFound(String.format("Can't instantiate provider, as there's no factory with alias %s", row.getFactoryAlias())); } catch (AuthorizationSetupException ex) { logger.log(Level.WARNING, "Error instantiating authentication provider: " + ex.getMessage(), ex); return error(Status.INTERNAL_SERVER_ERROR, String.format("Can't instantiate provider: %s", ex.getMessage())); } } else { // disable a provider authSvc.deregisterProvider(id); return ok("Authentication Provider '" + id + "' disabled. " + ( authSvc.getAuthenticationProviderIds().isEmpty() ? "WARNING: no enabled authentication providers left." : "") ); } } @DELETE @Path("authenticationProviders/{id}/") public Response deleteAuthenticationProvider( @PathParam("id") String id ) { authSvc.deregisterProvider(id); AuthenticationProviderRow row = em.find(AuthenticationProviderRow.class, id); if ( row != null ) { em.remove( row ); } return ok("AuthenticationProvider " + id + " deleted. " + ( authSvc.getAuthenticationProviderIds().isEmpty() ? "WARNING: no enabled authentication providers left." : "")); } @GET @Path("authenticatedUsers/{identifier}/") public Response getAuthenticatedUser(@PathParam("identifier") String identifier) { AuthenticatedUser authenticatedUser = authSvc.getAuthenticatedUser(identifier); if (authenticatedUser != null) { return ok(jsonForAuthUser(authenticatedUser)); } return error(Response.Status.BAD_REQUEST, "User " + identifier + " not found."); } @DELETE @Path("authenticatedUsers/{identifier}/") public Response deleteAuthenticatedUser(@PathParam("identifier") String identifier) { AuthenticatedUser user = authSvc.getAuthenticatedUser(identifier); if (user!=null) { authSvc.deleteAuthenticatedUser(user.getId()); return ok("AuthenticatedUser " +identifier + " deleted. "); } return error(Response.Status.BAD_REQUEST, "User "+ identifier+" not found."); } @POST @Path("publishDataverseAsCreator/{id}") public Response publishDataverseAsCreator(@PathParam("id") long id) { try { Dataverse dataverse = dataverseSvc.find(id); if (dataverse != null) { AuthenticatedUser authenticatedUser = dataverse.getCreator(); return ok(json(execCommand(new PublishDataverseCommand(createDataverseRequest(authenticatedUser), dataverse)))); } else { return error(Status.BAD_REQUEST, "Could not find dataverse with id " + id); } } catch (WrappedResponse wr) { return wr.getResponse(); } } @GET @Path("authenticatedUsers") public Response listAuthenticatedUsers() { try { AuthenticatedUser user = findAuthenticatedUserOrDie(); if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "Superusers only."); } } catch (WrappedResponse ex) { return error(Response.Status.FORBIDDEN, "Superusers only."); } JsonArrayBuilder userArray = Json.createArrayBuilder(); authSvc.findAllAuthenticatedUsers().stream().forEach((user) -> { userArray.add(jsonForAuthUser(user)); }); return ok(userArray); } /** * curl -X PUT -d "shib@mailinator.com" * http://localhost:8080/api/admin/authenticatedUsers/id/11/convertShibToBuiltIn */ @PUT @Path("authenticatedUsers/id/{id}/convertShibToBuiltIn") public Response convertShibUserToBuiltin(@PathParam("id") Long id, String newEmailAddress) { try { AuthenticatedUser user = findAuthenticatedUserOrDie(); if (!user.isSuperuser()) { return error(Response.Status.FORBIDDEN, "Superusers only."); } } catch (WrappedResponse ex) { return error(Response.Status.FORBIDDEN, "Superusers only."); } try { BuiltinUser builtinUser = authSvc.convertShibToBuiltIn(id, newEmailAddress); if (builtinUser == null) { return error(Response.Status.BAD_REQUEST, "User id " + id + " could not be converted from Shibboleth to BuiltIn. An Exception was not thrown."); } JsonObjectBuilder output = Json.createObjectBuilder(); output.add("email", builtinUser.getEmail()); output.add("username", builtinUser.getUserName()); return ok(output); } catch (Throwable ex) { StringBuilder sb = new StringBuilder(); sb.append(ex + " "); while (ex.getCause() != null) { ex = ex.getCause(); sb.append(ex + " "); } String msg = "User id " + id + " could not be converted from Shibboleth to BuiltIn. Details from Exception: " + sb; logger.info(msg); return error(Response.Status.BAD_REQUEST, msg); } } /** * This is used in testing via AdminIT.java but we don't expect sysadmins to * use this. */ @Path("authenticatedUsers/convert/builtin2shib") @PUT public Response builtin2shib(String content) { logger.info("entering builtin2shib..."); try { AuthenticatedUser userToRunThisMethod = findAuthenticatedUserOrDie(); if (!userToRunThisMethod.isSuperuser()) { return error(Response.Status.FORBIDDEN, "Superusers only."); } } catch (WrappedResponse ex) { return error(Response.Status.FORBIDDEN, "Superusers only."); } boolean disabled = false; if (disabled) { return error(Response.Status.BAD_REQUEST, "API endpoint disabled."); } AuthenticatedUser builtInUserToConvert = null; String emailToFind; String password; String authuserId = "0"; // could let people specify id on authuser table. probably better to let them tell us their String newEmailAddressToUse; try { String[] args = content.split(":"); emailToFind = args[0]; password = args[1]; newEmailAddressToUse = args[2]; // authuserId = args[666]; } catch (ArrayIndexOutOfBoundsException ex) { return error(Response.Status.BAD_REQUEST, "Problem with content <<<" + content + ">>>: " + ex.toString()); } AuthenticatedUser existingAuthUserFoundByEmail = shibService.findAuthUserByEmail(emailToFind); String existing = "NOT FOUND"; if (existingAuthUserFoundByEmail != null) { builtInUserToConvert = existingAuthUserFoundByEmail; existing = existingAuthUserFoundByEmail.getIdentifier(); } else { long longToLookup = Long.parseLong(authuserId); AuthenticatedUser specifiedUserToConvert = authSvc.findByID(longToLookup); if (specifiedUserToConvert != null) { builtInUserToConvert = specifiedUserToConvert; } else { return error(Response.Status.BAD_REQUEST, "No user to convert. We couldn't find a *single* existing user account based on " + emailToFind + " and no user was found using specified id " + longToLookup); } } String shibProviderId = ShibAuthenticationProvider.PROVIDER_ID; Map<String, String> randomUser = shibService.getRandomUser(); // String eppn = UUID.randomUUID().toString().substring(0, 8); String eppn = randomUser.get("eppn"); String idPEntityId = randomUser.get("idp"); String notUsed = null; String separator = "|"; UserIdentifier newUserIdentifierInLookupTable = new UserIdentifier(idPEntityId + separator + eppn, notUsed); String overwriteFirstName = randomUser.get("firstName"); String overwriteLastName = randomUser.get("lastName"); String overwriteEmail = randomUser.get("email"); overwriteEmail = newEmailAddressToUse; logger.info("overwriteEmail: " + overwriteEmail); boolean validEmail = EMailValidator.isEmailValid(overwriteEmail, null); if (!validEmail) { // See https://github.com/IQSS/dataverse/issues/2998 return error(Response.Status.BAD_REQUEST, "invalid email: " + overwriteEmail); } /** * @todo If affiliation is not null, put it in RoleAssigneeDisplayInfo * constructor. */ /** * Here we are exercising (via an API test) shibService.getAffiliation * with the TestShib IdP and a non-production DevShibAccountType. */ idPEntityId = ShibUtil.testShibIdpEntityId; String overwriteAffiliation = shibService.getAffiliation(idPEntityId, ShibServiceBean.DevShibAccountType.RANDOM); logger.info("overwriteAffiliation: " + overwriteAffiliation); /** * @todo Find a place to put "position" in the authenticateduser table: * https://github.com/IQSS/dataverse/issues/1444#issuecomment-74134694 */ String overwritePosition = "staff;student"; AuthenticatedUserDisplayInfo displayInfo = new AuthenticatedUserDisplayInfo(overwriteFirstName, overwriteLastName, overwriteEmail, overwriteAffiliation, overwritePosition); JsonObjectBuilder response = Json.createObjectBuilder(); JsonArrayBuilder problems = Json.createArrayBuilder(); if (password != null) { response.add("password supplied", password); boolean knowsExistingPassword = false; BuiltinUser oldBuiltInUser = builtinUserService.findByUserName(builtInUserToConvert.getUserIdentifier()); if (oldBuiltInUser != null) { String usernameOfBuiltinAccountToConvert = oldBuiltInUser.getUserName(); response.add("old username", usernameOfBuiltinAccountToConvert); AuthenticatedUser authenticatedUser = shibService.canLogInAsBuiltinUser(usernameOfBuiltinAccountToConvert, password); if (authenticatedUser != null) { knowsExistingPassword = true; AuthenticatedUser convertedUser = authSvc.convertBuiltInToShib(builtInUserToConvert, shibProviderId, newUserIdentifierInLookupTable); if (convertedUser != null) { /** * @todo Display name is not being overwritten. Logic * must be in Shib backing bean */ AuthenticatedUser updatedInfoUser = authSvc.updateAuthenticatedUser(convertedUser, displayInfo); if (updatedInfoUser != null) { response.add("display name overwritten with", updatedInfoUser.getName()); } else { problems.add("couldn't update display info"); } } else { problems.add("unable to convert user"); } } } else { problems.add("couldn't find old username"); } if (!knowsExistingPassword) { String message = "User doesn't know password."; problems.add(message); /** * @todo Someday we should make a errorResponse method that * takes JSON arrays and objects. */ return error(Status.BAD_REQUEST, problems.build().toString()); } // response.add("knows existing password", knowsExistingPassword); } response.add("user to convert", builtInUserToConvert.getIdentifier()); response.add("existing user found by email (prompt to convert)", existing); response.add("changing to this provider", shibProviderId); response.add("value to overwrite old first name", overwriteFirstName); response.add("value to overwrite old last name", overwriteLastName); response.add("value to overwrite old email address", overwriteEmail); if (overwriteAffiliation != null) { response.add("affiliation", overwriteAffiliation); } response.add("problems", problems); return ok(response); } @DELETE @Path("authenticatedUsers/id/{id}/") public Response deleteAuthenticatedUserById(@PathParam("id") Long id) { AuthenticatedUser user = authSvc.findByID(id); if (user != null) { authSvc.deleteAuthenticatedUser(user.getId()); return ok("AuthenticatedUser " + id + " deleted. "); } return error(Response.Status.BAD_REQUEST, "User " + id + " not found."); } @Path("roles") @POST public Response createNewBuiltinRole(RoleDTO roleDto) { ActionLogRecord alr = new ActionLogRecord(ActionLogRecord.ActionType.Admin, "createBuiltInRole") .setInfo(roleDto.getAlias() + ":" + roleDto.getDescription() ); try { return ok(json(rolesSvc.save(roleDto.asRole()))); } catch (Exception e) { alr.setActionResult(ActionLogRecord.Result.InternalError); alr.setInfo( alr.getInfo() + "// " + e.getMessage() ); return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); } finally { actionLogSvc.log(alr); } } @Path("roles") @GET public Response listBuiltinRoles() { try { return ok( rolesToJson(rolesSvc.findBuiltinRoles()) ); } catch (Exception e) { return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); } } @Path("superuser/{identifier}") @POST public Response toggleSuperuser(@PathParam("identifier") String identifier) { ActionLogRecord alr = new ActionLogRecord(ActionLogRecord.ActionType.Admin, "toggleSuperuser") .setInfo( identifier ); try { AuthenticatedUser user = authSvc.getAuthenticatedUser(identifier); user.setSuperuser(!user.isSuperuser()); return ok("User " + user.getIdentifier() + " " + (user.isSuperuser() ? "set": "removed") + " as a superuser."); } catch (Exception e) { alr.setActionResult(ActionLogRecord.Result.InternalError); alr.setInfo( alr.getInfo() + "// " + e.getMessage() ); return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); } finally { actionLogSvc.log(alr); } } @Path("validate") @GET public Response validate() { String msg = "UNKNOWN"; try { beanValidationSvc.validateDatasets(); msg = "valid"; } catch (Exception ex) { Throwable cause = ex; while (cause != null) { if (cause instanceof ConstraintViolationException) { ConstraintViolationException constraintViolationException = (ConstraintViolationException) cause; for (ConstraintViolation<?> constraintViolation : constraintViolationException.getConstraintViolations()) { String databaseRow = constraintViolation.getLeafBean().toString(); String field = constraintViolation.getPropertyPath().toString(); String invalidValue = constraintViolation.getInvalidValue().toString(); JsonObjectBuilder violation = Json.createObjectBuilder(); violation.add("entityClassDatabaseTableRowId", databaseRow); violation.add("field", field); violation.add("invalidValue", invalidValue); return ok(violation); } } cause = cause.getCause(); } } return ok(msg); } /** * This method is used in integration tests. * * @param userId The database id of an AuthenticatedUser. * @return The confirm email token. */ @Path("confirmEmail/{userId}") @GET public Response getConfirmEmailToken(@PathParam("userId") long userId) { AuthenticatedUser user = authSvc.findByID(userId); if (user != null) { ConfirmEmailData confirmEmailData = confirmEmailSvc.findSingleConfirmEmailDataByUser(user); if (confirmEmailData != null) { return ok(Json.createObjectBuilder().add("token", confirmEmailData.getToken())); } } return error(Status.BAD_REQUEST, "Could not find confirm email token for user " + userId); } /** * This method is used in integration tests. * * @param userId The database id of an AuthenticatedUser. */ @Path("confirmEmail/{userId}") @POST public Response startConfirmEmailProcess(@PathParam("userId") long userId) { AuthenticatedUser user = authSvc.findByID(userId); if (user != null) { try { ConfirmEmailInitResponse confirmEmailInitResponse = confirmEmailSvc.beginConfirm(user); ConfirmEmailData confirmEmailData = confirmEmailInitResponse.getConfirmEmailData(); return ok( Json.createObjectBuilder() .add("tokenCreated", confirmEmailData.getCreated().toString()) .add("identifier", user.getUserIdentifier() )); } catch (ConfirmEmailException ex) { return error(Status.BAD_REQUEST, "Could not start confirm email process for user " + userId + ": " + ex.getLocalizedMessage()); } } return error(Status.BAD_REQUEST, "Could not find user based on " + userId); } /** * This method is used by an integration test in UsersIT.java to exercise * bug https://github.com/IQSS/dataverse/issues/3287 . Not for use by users! */ @Path("convertUserFromBcryptToSha1") @POST public Response convertUserFromBcryptToSha1(String json) { JsonReader jsonReader = Json.createReader(new StringReader(json)); JsonObject object = jsonReader.readObject(); jsonReader.close(); BuiltinUser builtinUser = builtinUserService.find(new Long(object.getInt("builtinUserId"))); builtinUser.updateEncryptedPassword("4G7xxL9z11/JKN4jHPn4g9iIQck=", 0); // password is "sha-1Pass", 0 means SHA-1 BuiltinUser savedUser = builtinUserService.save(builtinUser); return ok("foo: " + savedUser); } @Path("assignments/assignees/{raIdtf: .*}") @GET public Response getAssignmentsFor( @PathParam("raIdtf") String raIdtf ) { JsonArrayBuilder arr = Json.createArrayBuilder(); roleAssigneeSvc.getAssignmentsFor(raIdtf).forEach( a -> arr.add(json(a))); return ok(arr); } @Path("permissions/{dvo}") @GET public Response findPermissonsOn(@PathParam("dvo") String dvo) { try { DvObject dvObj = findDvo(dvo); if (dvObj == null) { return notFound("DvObject " + dvo + " not found"); } try { User aUser = findUserOrDie(); JsonObjectBuilder bld = Json.createObjectBuilder(); bld.add("user", aUser.getIdentifier()); bld.add("permissions", json(permissionSvc.permissionsFor(createDataverseRequest(aUser), dvObj))); return ok(bld); } catch (WrappedResponse wr) { return wr.getResponse(); } } catch (Exception e) { logger.log(Level.SEVERE, "Error while testing permissions", e); return error(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()); } } @Path("assignee/{idtf}") @GET public Response findRoleAssignee(@PathParam("idtf") String idtf) { RoleAssignee ra = roleAssigneeSvc.getRoleAssignee(idtf); return (ra == null) ? notFound("Role Assignee '" + idtf + "' not found.") : ok(json(ra.getDisplayInfo())); } }