diff --git a/.gitignore b/.gitignore index af46d9555b..e75f10cb9f 100644 --- a/.gitignore +++ b/.gitignore @@ -57,5 +57,6 @@ dump.rdb dependencies.txt /client-test-data.zip /db_staging_dump.trig -cleandeps.sh +/.vscode +/cleandeps.sh /.metals diff --git a/webapi/scripts/expected-client-test-data.txt b/webapi/scripts/expected-client-test-data.txt index 7376daadb8..13d4f03688 100644 --- a/webapi/scripts/expected-client-test-data.txt +++ b/webapi/scripts/expected-client-test-data.txt @@ -137,7 +137,11 @@ test-data/admin/users/ test-data/admin/users/add-user-to-group-response.json test-data/admin/users/add-user-to-project-admin-group-response.json test-data/admin/users/add-user-to-project-response.json +test-data/admin/users/create-user-request-duplicate-email.json +test-data/admin/users/create-user-request-duplicate-username.json test-data/admin/users/create-user-request.json +test-data/admin/users/create-user-response-duplicate-email.json +test-data/admin/users/create-user-response-duplicate-username.json test-data/admin/users/create-user-response.json test-data/admin/users/create-user-with-custom-Iri-request.json test-data/admin/users/create-user-with-custom-Iri-response.json @@ -153,17 +157,25 @@ test-data/admin/users/get-user-response.json test-data/admin/users/get-users-for-ProjectAdmin-response.json test-data/admin/users/get-users-for-SystemAdmin-response.json test-data/admin/users/get-users-response.json +test-data/admin/users/incomplete-update-user-password-request-2.json +test-data/admin/users/incomplete-update-user-password-request.json +test-data/admin/users/incomplete-update-user-password-response-2.json +test-data/admin/users/incomplete-update-user-password-response.json test-data/admin/users/remove-user-from-group-response.json test-data/admin/users/remove-user-from-project-admin-group-response.json test-data/admin/users/remove-user-from-project-response.json test-data/admin/users/update-user-password-request.json test-data/admin/users/update-user-password-response.json +test-data/admin/users/update-user-request-without-iri.json test-data/admin/users/update-user-request.json +test-data/admin/users/update-user-response-without-iri-1.json +test-data/admin/users/update-user-response-without-iri-2.json test-data/admin/users/update-user-response.json test-data/admin/users/update-user-status-request.json test-data/admin/users/update-user-status-response.json test-data/admin/users/update-user-system-admin-membership-request.json test-data/admin/users/update-user-system-admin-membership-response.json +test-data/admin/users/user-already-member-of-project-response.json test-data/system/ test-data/system/health/ test-data/system/health/maintenance-mode-response.json diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsMessagesADM.scala index a8a58d51c7..3097a1bd59 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/groupsmessages/GroupsMessagesADM.scala @@ -89,7 +89,7 @@ case class ChangeGroupApiRequestADM(name: Option[String] = None, if (parametersCount == 0) throw BadRequestException("No data sent in API request.") /** - * check that only allowed information for the 2 cases is send and not more. + * check that only allowed information for the 2 cases is sent and not more. */ // change status case if (status.isDefined) { diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/AdminEntities.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/AdminEntities.scala new file mode 100644 index 0000000000..8f5f8f1d53 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/AdminEntities.scala @@ -0,0 +1,72 @@ +package org.knora.webapi.messages.admin.responder.usersmessages + +import org.knora.webapi.IRI + +sealed trait ValidationError +case object InvalidUsername extends ValidationError +case object InvalidEmail extends ValidationError +case object InvalidGivenOrFamilyName extends ValidationError +case object InvalidPassword extends ValidationError +case object InvalidLanguageCode extends ValidationError + +trait UserCreatePayloadTraitADM { + def create( + id: Option[IRI], + username: Username, + email: Email, + givenName: GivenName, + familyName: FamilyName, + password: Password, + status: Status, + lang: LanguageCode, + systemAdmin: SystemAdmin + ): UserCreatePayloadADM +} + +/** + * User entity representing the payload for the create user request + */ +sealed abstract case class UserCreatePayloadADM( + id: Option[IRI], + username: Option[Username], + email: Option[Email], + givenName: Option[GivenName], + familyName: Option[FamilyName], + password: Option[Password], + status: Option[Status], + lang: Option[LanguageCode], + projects: Option[Seq[IRI]], + projectsAdmin: Option[Seq[IRI]], + groups: Option[Seq[IRI]], + systemAdmin: Option[SystemAdmin] +) + +object UserCreatePayloadADM extends UserCreatePayloadTraitADM { + + /** The create constructor needs all attributes but id which is optional */ + override def create( + id: Option[IRI] = None, + username: Username, + email: Email, + givenName: GivenName, + familyName: FamilyName, + password: Password, + status: Status, + lang: LanguageCode, + systemAdmin: SystemAdmin + ): UserCreatePayloadADM = + new UserCreatePayloadADM( + id = id, + username = Some(username), + email = Some(email), + givenName = Some(givenName), + familyName = Some(familyName), + password = Some(password), + status = Some(status), + lang = Some(lang), + projects = None, + projectsAdmin = None, + groups = None, + systemAdmin = Some(systemAdmin) + ) {} +} diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADM.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADM.scala index 5dd282cc91..cbc1da7122 100644 --- a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADM.scala @@ -19,8 +19,6 @@ package org.knora.webapi.messages.admin.responder.usersmessages -import java.util.UUID - import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import org.knora.webapi._ import org.knora.webapi.exceptions.{BadRequestException, DataConversionException, InconsistentRepositoryDataException} @@ -35,6 +33,8 @@ import org.knora.webapi.messages.v1.responder.usermessages._ import org.knora.webapi.messages.{OntologyConstants, StringFormatter} import spray.json._ +import java.util.UUID + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // API requests @@ -63,33 +63,18 @@ case class CreateUserApiRequestADM( systemAdmin: Boolean ) { - implicit protected val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance - def toJsValue: JsValue = UsersADMJsonProtocol.createUserApiRequestADMFormat.write(this) - - // check for required information - if (username.isEmpty) throw BadRequestException("Username cannot be empty") - if (email.isEmpty) throw BadRequestException("Email cannot be empty") - if (password.isEmpty) throw BadRequestException("Password cannot be empty") - if (givenName.isEmpty) throw BadRequestException("Given name cannot be empty") - if (familyName.isEmpty) throw BadRequestException("Family name cannot be empty") - - //check the custom Iri - stringFormatter.validateOptionalUserIri(id, throw BadRequestException(s"Invalid user IRI")) } /** * Represents an API request payload that asks the Knora API server to update an existing user. Information that can - * be changed include the user's username, email, given name, family name, language, password, user status, and system admin - * membership. + * be changed are: user's username, email, given name, family name, language, user status, and system admin membership. * * @param username the new username. Needs to be unique on the server. * @param email the new email address. Needs to be unique on the server. * @param givenName the new given name. * @param familyName the new family name. * @param lang the new ISO 639-1 code of the new preferred language. - * @param requesterPassword the password of the user making the request. - * @param newPassword the new password. * @param status the new user status (active = true, inactive = false). * @param systemAdmin the new system admin membership status. */ @@ -99,8 +84,6 @@ case class ChangeUserApiRequestADM( givenName: Option[String] = None, familyName: Option[String] = None, lang: Option[String] = None, - requesterPassword: Option[String] = None, - newPassword: Option[String] = None, status: Option[Boolean] = None, systemAdmin: Option[Boolean] = None ) { @@ -111,49 +94,40 @@ case class ChangeUserApiRequestADM( givenName, familyName, lang, - requesterPassword, - newPassword, status, systemAdmin ).flatten.size - // println(requesterPassword + " " + newPassword) - // something needs to be sent, i.e. everything 'None' is not allowed if (parametersCount == 0) throw BadRequestException("No data sent in API request.") - /* check that only allowed information for the 4 cases is send and not more. */ - - // change password case - if (requesterPassword.isDefined || newPassword.isDefined) { - if (parametersCount > 2) { - throw BadRequestException("Too many parameters sent for password change.") - } else if (parametersCount < 2) { - throw BadRequestException("Too few parameters sent for password change.") - } - } + /* check that only allowed information for the 3 cases (changing status, systemAdmin and basic information) is sent and not more. */ // change status case if (status.isDefined) { - if (parametersCount > 1) throw BadRequestException("Too many parameters sent for user status change.") + if (parametersCount > 1) throw BadRequestException("Too many parameters sent for change request.") } // change system admin membership case if (systemAdmin.isDefined) { - if (parametersCount > 1) throw BadRequestException("Too many parameters sent for system admin membership change.") + if (parametersCount > 1) throw BadRequestException("Too many parameters sent for change request.") } // change basic user information case - if (parametersCount > 5) throw BadRequestException("Too many parameters sent for basic user information change.") + if (parametersCount > 5) throw BadRequestException("Too many parameters sent for change request.") def toJsValue: JsValue = UsersADMJsonProtocol.changeUserApiRequestADMFormat.write(this) } +case class ChangeUserPasswordApiRequestADM(requesterPassword: Option[String], newPassword: Option[String]) { + def toJsValue: JsValue = UsersADMJsonProtocol.changeUserPasswordApiRequestADMFormat.write(this) +} + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Messages /** - * An abstract trait representing message that can be sent to `UsersResponderV1`. + * An abstract trait representing message that can be sent to `UsersResponderADM`. */ sealed trait UsersResponderRequestADM extends KnoraRequestADM @@ -172,7 +146,7 @@ case class UsersGetADM( ) extends UsersResponderRequestADM /** - * Get all information about all users in form of [[UsersGetResponseV1]]. The UsersGetRequestV1 returns either + * Get all information about all users in form of [[UsersGetResponseADM]]. The UsersResponderRequestADM returns either * something or a NotFound exception if there are no users found. Administration permission checking is performed. * * @param userInformationTypeADM the extent of the information returned. @@ -218,13 +192,13 @@ case class UserGetRequestADM( /** * Requests the creation of a new user. * - * @param createRequest the [[CreateUserApiRequestADM]] information used for creating the new user. + * @param userCreatePayloadADM the [[UserCreatePayloadADM]] information used for creating the new user. * @param featureFactoryConfig the feature factory configuration. * @param requestingUser the user creating the new user. * @param apiRequestID the ID of the API request. */ case class UserCreateRequestADM( - createRequest: CreateUserApiRequestADM, + userCreatePayloadADM: UserCreatePayloadADM, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM, apiRequestID: UUID @@ -234,14 +208,14 @@ case class UserCreateRequestADM( * Request updating of an existing user. * * @param userIri the IRI of the user to be updated. - * @param changeUserRequest the data which needs to be update. + * @param userUpdateBasicInformationPayload the [[UserUpdateBasicInformationPayloadADM]] object containing the data to be updated. * @param featureFactoryConfig the feature factory configuration. * @param requestingUser the user initiating the request. * @param apiRequestID the ID of the API request. */ -case class UserChangeBasicUserInformationRequestADM( +case class UserChangeBasicInformationRequestADM( userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, + userUpdateBasicInformationPayload: UserUpdateBasicInformationPayloadADM, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM, apiRequestID: UUID @@ -251,14 +225,14 @@ case class UserChangeBasicUserInformationRequestADM( * Request updating the users password. * * @param userIri the IRI of the user to be updated. - * @param changeUserRequest the [[ChangeUserApiRequestADM]] object containing the old and new password. + * @param userUpdatePasswordPayload the [[UserUpdatePasswordPayloadADM]] object containing the old and new password. * @param featureFactoryConfig the feature factory configuration. * @param requestingUser the user initiating the request. * @param apiRequestID the ID of the API request. */ case class UserChangePasswordRequestADM( userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, + userUpdatePasswordPayload: UserUpdatePasswordPayloadADM, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM, apiRequestID: UUID @@ -268,14 +242,14 @@ case class UserChangePasswordRequestADM( * Request updating the users status ('knora-base:isActiveUser' property) * * @param userIri the IRI of the user to be updated. - * @param changeUserRequest the [[ChangeUserApiRequestADM]] containing the new status (true / false). + * @param status the [[Status]] containing the new status (true / false). * @param featureFactoryConfig the feature factory configuration. * @param requestingUser the user initiating the request. * @param apiRequestID the ID of the API request. */ case class UserChangeStatusRequestADM( userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, + status: Status, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM, apiRequestID: UUID @@ -285,15 +259,14 @@ case class UserChangeStatusRequestADM( * Request updating the users system admin status ('knora-base:isInSystemAdminGroup' property) * * @param userIri the IRI of the user to be updated. - * @param changeUserRequest the [[ChangeUserApiRequestADM]] containing - * the new system admin membership status (true / false). + * @param systemAdmin the [[SystemAdmin]] value object containing the new system admin membership status (true / false). * @param featureFactoryConfig the feature factory configuration. * @param requestingUser the user initiating the request. * @param apiRequestID the ID of the API request. */ case class UserChangeSystemAdminMembershipStatusRequestADM( userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, + systemAdmin: SystemAdmin, featureFactoryConfig: FeatureFactoryConfig, requestingUser: UserADM, apiRequestID: UUID @@ -450,7 +423,7 @@ case class UserGroupMembershipRemoveRequestADM( * @param users a sequence of user profiles of the requested type. */ case class UsersGetResponseADM(users: Seq[UserADM]) extends KnoraResponseADM { - def toJsValue = UsersADMJsonProtocol.usersGetResponseADMFormat.write(this) + def toJsValue: JsValue = UsersADMJsonProtocol.usersGetResponseADMFormat.write(this) } /** @@ -553,12 +526,12 @@ case class UserADM( // SCrypt import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder val encoder = new SCryptPasswordEncoder() - encoder.matches(password, hashedPassword.toString) + encoder.matches(password, hashedPassword) } else if (hashedPassword.startsWith("$2a$")) { // BCrypt import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder val encoder = new BCryptPasswordEncoder() - encoder.matches(password, hashedPassword.toString) + encoder.matches(password, hashedPassword) } else { // SHA-1 val md = java.security.MessageDigest.getInstance("SHA-1") @@ -573,7 +546,7 @@ case class UserADM( */ def ofType(userTemplateType: UserInformationTypeADM): UserADM = userTemplateType match { - case UserInformationTypeADM.PUBLIC => { + case UserInformationTypeADM.PUBLIC => UserADM( id = id, username = "", @@ -589,9 +562,7 @@ case class UserADM( sessionId = None, permissions = PermissionsDataADM() ) - } - case UserInformationTypeADM.SHORT => { - + case UserInformationTypeADM.SHORT => UserADM( id = id, username = username, @@ -607,9 +578,7 @@ case class UserADM( sessionId = None, permissions = PermissionsDataADM() ) - } - case UserInformationTypeADM.RESTRICTED => { - + case UserInformationTypeADM.RESTRICTED => UserADM( id = id, username = username, @@ -625,8 +594,7 @@ case class UserADM( sessionId = None, permissions = permissions ) - } - case UserInformationTypeADM.FULL => { + case UserInformationTypeADM.FULL => UserADM( id = id, username = username, @@ -642,7 +610,6 @@ case class UserADM( sessionId = sessionId, permissions = permissions ) - } case _ => throw BadRequestException(s"The requested userTemplateType: $userTemplateType is invalid.") } @@ -668,8 +635,6 @@ case class UserADM( def isSystemUser: Boolean = id.equalsIgnoreCase(OntologyConstants.KnoraAdmin.SystemUser) - def isAnonymousUser: Boolean = id.equalsIgnoreCase(OntologyConstants.KnoraAdmin.AnonymousUser) - def fullname: String = givenName + " " + familyName def getDigest: String = { @@ -744,6 +709,8 @@ case class UserADM( status = Some(status), lang = lang ) + + def isAnonymousUser: Boolean = id.equalsIgnoreCase(OntologyConstants.KnoraAdmin.AnonymousUser) } /** @@ -763,10 +730,10 @@ object UserInformationTypeADM extends Enumeration { type UserInformationTypeADM = Value - val PUBLIC = Value(0, "public") // a temporary type which only returns firstname and lastname - val SHORT = Value(1, "short") // only basic user information (restricted and additionally without groups - val RESTRICTED = Value(2, "restricted") // without sensitive information - val FULL = Value(3, "full") // everything, including sensitive information + val PUBLIC: Value = Value(0, "public") // a temporary type which only returns firstname and lastname + val SHORT: Value = Value(1, "short") // only basic user information (restricted and additionally without groups + val RESTRICTED: Value = Value(2, "restricted") // without sensitive information + val FULL: Value = Value(3, "full") // everything, including sensitive information val valueMap: Map[String, Value] = values.map(v => (v.toString, v)).toMap @@ -791,9 +758,9 @@ object UserIdentifierType extends Enumeration { type UserIdentifierType - val IRI = Value(0, "iri") - val EMAIL = Value(1, "email") - val USERNAME = Value(3, "username") + val IRI: Value = Value(0, "iri") + val EMAIL: Value = Value(1, "email") + val USERNAME: Value = Value(3, "username") } /** @@ -911,13 +878,12 @@ class UserIdentifierADM private ( } /** - * Payload used for updating of an existing user. + * Payload used for updating an existing user. * - * @param email the new email address. Needs to be unique on the server. * @param username the new username. + * @param email the new email address. Needs to be unique on the server. * @param givenName the new given name. * @param familyName the new family name. - * @param password the new password. * @param status the new status. * @param lang the new language. * @param projects the new project memberships list. @@ -925,25 +891,24 @@ class UserIdentifierADM private ( * @param groups the new group memberships list. * @param systemAdmin the new system admin membership */ -case class UserUpdatePayloadADM( - username: Option[String] = None, - email: Option[String] = None, - givenName: Option[String] = None, - familyName: Option[String] = None, - password: Option[String] = None, - status: Option[Boolean] = None, - lang: Option[String] = None, +case class UserChangeRequestADM( + username: Option[Username] = None, + email: Option[Email] = None, + givenName: Option[GivenName] = None, + familyName: Option[FamilyName] = None, + status: Option[Status] = None, + lang: Option[LanguageCode] = None, projects: Option[Seq[IRI]] = None, projectsAdmin: Option[Seq[IRI]] = None, groups: Option[Seq[IRI]] = None, - systemAdmin: Option[Boolean] = None + systemAdmin: Option[SystemAdmin] = None ) { val parametersCount: Int = List( + username, email, givenName, familyName, - password, status, lang, projects, @@ -957,13 +922,6 @@ case class UserUpdatePayloadADM( throw BadRequestException("No data sent in API request.") } - /* check that only allowed information for the 4 cases is send and not more. */ - - // change password case - if (password.isDefined && parametersCount > 1) { - throw BadRequestException("Too many parameters sent for password change.") - } - // change status case if (status.isDefined && parametersCount > 1) { throw BadRequestException("Too many parameters sent for user status change.") @@ -990,12 +948,44 @@ case class UserUpdatePayloadADM( } // change basic user information case - if (parametersCount > 4) { + if (parametersCount > 5) { throw BadRequestException("Too many parameters sent for basic user information change.") } +} +/** + * Payload used for updating basic information of an existing user. + * + * @param username the new username. + * @param email the new email address. Needs to be unique on the server. + * @param givenName the new given name. + * @param familyName the new family name. + * @param lang the new language. + */ +case class UserUpdateBasicInformationPayloadADM( + username: Option[Username] = None, + email: Option[Email] = None, + givenName: Option[GivenName] = None, + familyName: Option[FamilyName] = None, + lang: Option[LanguageCode] = None +) { + + val parametersCount: Int = List( + username, + email, + givenName, + familyName, + lang + ).flatten.size + + // something needs to be sent, i.e. everything 'None' is not allowed + if (parametersCount == 0) { + throw BadRequestException("No data sent in API request.") + } } +case class UserUpdatePasswordPayloadADM(requesterPassword: Password, newPassword: Password) {} + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // JSON formatting @@ -1022,17 +1012,12 @@ object UsersADMJsonProtocol "lang", "systemAdmin" ) - implicit val changeUserApiRequestADMFormat: RootJsonFormat[ChangeUserApiRequestADM] = jsonFormat( - ChangeUserApiRequestADM, - "username", - "email", - "givenName", - "familyName", - "lang", + implicit val changeUserApiRequestADMFormat: RootJsonFormat[ChangeUserApiRequestADM] = + jsonFormat(ChangeUserApiRequestADM, "username", "email", "givenName", "familyName", "lang", "status", "systemAdmin") + implicit val changeUserPasswordApiRequestADMFormat: RootJsonFormat[ChangeUserPasswordApiRequestADM] = jsonFormat( + ChangeUserPasswordApiRequestADM, "requesterPassword", - "newPassword", - "status", - "systemAdmin" + "newPassword" ) implicit val usersGetResponseADMFormat: RootJsonFormat[UsersGetResponseADM] = jsonFormat1(UsersGetResponseADM) implicit val userProfileResponseADMFormat: RootJsonFormat[UserResponseADM] = jsonFormat1(UserResponseADM) diff --git a/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/ValueObjects.scala b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/ValueObjects.scala new file mode 100644 index 0000000000..70a807dee1 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/messages/admin/responder/usersmessages/ValueObjects.scala @@ -0,0 +1,146 @@ +package org.knora.webapi.messages.admin.responder.usersmessages + +import org.knora.webapi.LanguageCodes +import org.knora.webapi.exceptions.BadRequestException + +import scala.util.matching.Regex + +trait ValueObject[T, K] { + def create(value: K): Either[Throwable, T] +} + +/** + * Username value object. + */ +sealed abstract case class Username(value: String) + +object Username extends ValueObject[Username, String] { + + /** + * A regex that matches a valid username + * - 4 - 50 characters long + * - Only contains alphanumeric characters, underscore and dot. + * - Underscore and dot can't be at the end or start of a username + * - Underscore or dot can't be used multiple times in a row + */ + private val UsernameRegex: Regex = + """^(?=.{4,50}$)(?![_.])(?!.*[_.]{2})[a-zA-Z0-9._]+(? + Right(new Username(value) {}) + case None => Left(BadRequestException("Invalid username")) + } + } + +} + +/** + * Email value object. + */ +sealed abstract case class Email(value: String) + +object Email extends ValueObject[Email, String] { + + private val EmailRegex: Regex = """^.+@.+$""".r // TODO use proper validation + + override def create(value: String): Either[Throwable, Email] = + if (value.isEmpty) { + Left(BadRequestException("Missing email")) + } else { + EmailRegex.findFirstIn(value) match { + case Some(value) => Right(new Email(value) {}) + case None => Left(BadRequestException("Invalid email")) + } + } +} + +/** + * Password value object. + */ +sealed abstract case class Password(value: String) + +object Password extends ValueObject[Password, String] { + + private val PasswordRegex: Regex = """^[\s\S]*$""".r //TODO: add password validation + + override def create(value: String): Either[Throwable, Password] = + if (value.isEmpty) { + Left(BadRequestException("Missing password")) + } else { + PasswordRegex.findFirstIn(value) match { + case Some(value) => Right(new Password(value) {}) + case None => Left(BadRequestException("Invalid password")) + } + } +} + +/** + * GivenName value object. + */ +sealed abstract case class GivenName(value: String) + +object GivenName extends ValueObject[GivenName, String] { + // TODO use proper validation for value + override def create(value: String): Either[Throwable, GivenName] = + if (value.isEmpty) { + Left(BadRequestException("Missing given name")) + } else { + Right(new GivenName(value) {}) + } +} + +/** + * FamilyName value object. + */ +sealed abstract case class FamilyName(value: String) + +object FamilyName extends ValueObject[FamilyName, String] { + // TODO use proper validation for value + override def create(value: String): Either[Throwable, FamilyName] = + if (value.isEmpty) { + Left(BadRequestException("Missing family name")) + } else { + Right(new FamilyName(value) {}) + } +} + +/** + * LanguageCode value object. + */ +sealed abstract case class LanguageCode(value: String) + +object LanguageCode extends ValueObject[LanguageCode, String] { + override def create(value: String): Either[Throwable, LanguageCode] = + if (value.isEmpty) { + Left(BadRequestException("Missing language code")) + } else if (!LanguageCodes.SupportedLanguageCodes.contains(value)) { + Left(BadRequestException("Invalid language code")) + } else { + Right(new LanguageCode(value) {}) + } +} + +/** + * Status value object. + */ +sealed abstract case class Status(value: Boolean) + +object Status extends ValueObject[Status, Boolean] { + override def create(value: Boolean): Either[Throwable, Status] = + Right(new Status(value) {}) +} + +/** + * SystemAdmin value object. + */ +sealed abstract case class SystemAdmin(value: Boolean) + +object SystemAdmin extends ValueObject[SystemAdmin, Boolean] { + override def create(value: Boolean): Either[Throwable, SystemAdmin] = + Right(new SystemAdmin(value) {}) +} diff --git a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala index 00c1dafd67..ba2cc76954 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/admin/UsersResponderADM.scala @@ -19,11 +19,10 @@ package org.knora.webapi.responders.admin -import java.util.UUID - import akka.http.scaladsl.util.FastFuture import akka.pattern._ -import org.knora.webapi.exceptions._ +import org.knora.webapi._ +import org.knora.webapi.exceptions.{BadRequestException, InconsistentRepositoryDataException, _} import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.instrumentation.InstrumentationSupport import org.knora.webapi.messages.IriConversions._ @@ -31,7 +30,7 @@ import org.knora.webapi.messages.admin.responder.groupsmessages.{GroupADM, Group import org.knora.webapi.messages.admin.responder.permissionsmessages.{PermissionDataGetADM, PermissionsDataADM} import org.knora.webapi.messages.admin.responder.projectsmessages.{ProjectADM, ProjectGetADM, ProjectIdentifierADM} import org.knora.webapi.messages.admin.responder.usersmessages.UserInformationTypeADM.UserInformationTypeADM -import org.knora.webapi.messages.admin.responder.usersmessages.{UserUpdatePayloadADM, _} +import org.knora.webapi.messages.admin.responder.usersmessages.{Password, UserChangeRequestADM, _} import org.knora.webapi.messages.store.cacheservicemessages.{ CacheServiceGetUserADM, CacheServicePutUserADM, @@ -44,22 +43,22 @@ import org.knora.webapi.messages.v1.responder.usermessages._ import org.knora.webapi.messages.{OntologyConstants, SmartIri} import org.knora.webapi.responders.Responder.handleUnexpectedMessage import org.knora.webapi.responders.{IriLocker, Responder} -import org.knora.webapi.{exceptions, _} import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import java.util.UUID import scala.concurrent.Future /** - * Provides information about Knora users to other responders. - */ + * Provides information about Knora users to other responders. + */ class UsersResponderADM(responderData: ResponderData) extends Responder(responderData) with InstrumentationSupport { // The IRI used to lock user creation and update private val USERS_GLOBAL_LOCK_IRI = "http://rdfh.ch/users" /** - * Receives a message extending [[UsersResponderRequestV1]], and returns an appropriate message. - */ + * Receives a message extending [[UsersResponderRequestADM]], and returns an appropriate message. + */ def receive(msg: UsersResponderRequestADM) = msg match { case UsersGetADM(userInformationTypeADM, featureFactoryConfig, requestingUser) => getAllUserADM(userInformationTypeADM, featureFactoryConfig, requestingUser) @@ -69,56 +68,82 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde getSingleUserADM(identifier, userInformationTypeADM, featureFactoryConfig, requestingUser) case UserGetRequestADM(identifier, userInformationTypeADM, featureFactoryConfig, requestingUser) => getSingleUserADMRequest(identifier, userInformationTypeADM, featureFactoryConfig, requestingUser) - case UserCreateRequestADM(createRequest, featureFactoryConfig, requestingUser, apiRequestID) => - createNewUserADM(createRequest, featureFactoryConfig, requestingUser, apiRequestID) - case UserChangeBasicUserInformationRequestADM(userIri, - changeUserRequest, - featureFactoryConfig, - requestingUser, - apiRequestID) => - changeBasicUserInformationADM(userIri, changeUserRequest, featureFactoryConfig, requestingUser, apiRequestID) - case UserChangePasswordRequestADM(userIri, changeUserRequest, featureFactoryConfig, requestingUser, apiRequestID) => - changePasswordADM(userIri, changeUserRequest, featureFactoryConfig, requestingUser, apiRequestID) - case UserChangeStatusRequestADM(userIri, changeUserRequest, featureFactoryConfig, requestingUser, apiRequestID) => - changeUserStatusADM(userIri, changeUserRequest, featureFactoryConfig, requestingUser, apiRequestID) - case UserChangeSystemAdminMembershipStatusRequestADM(userIri, - changeSystemAdminMembershipStatusRequest, - featureFactoryConfig, - requestingUser, - apiRequestID) => - changeUserSystemAdminMembershipStatusADM(userIri, - changeSystemAdminMembershipStatusRequest, - featureFactoryConfig, - requestingUser, - apiRequestID) + case UserCreateRequestADM(userCreatePayloadADM, featureFactoryConfig, requestingUser, apiRequestID) => + createNewUserADM(userCreatePayloadADM, featureFactoryConfig, requestingUser, apiRequestID) + case UserChangeBasicInformationRequestADM( + userIri, + userUpdateBasicInformationPayload, + featureFactoryConfig, + requestingUser, + apiRequestID + ) => + changeBasicUserInformationADM( + userIri, + userUpdateBasicInformationPayload, + featureFactoryConfig, + requestingUser, + apiRequestID + ) + case UserChangePasswordRequestADM( + userIri, + userUpdatePasswordPayload, + featureFactoryConfig, + requestingUser, + apiRequestID + ) => + changePasswordADM(userIri, userUpdatePasswordPayload, featureFactoryConfig, requestingUser, apiRequestID) + case UserChangeStatusRequestADM(userIri, status, featureFactoryConfig, requestingUser, apiRequestID) => + changeUserStatusADM(userIri, status, featureFactoryConfig, requestingUser, apiRequestID) + case UserChangeSystemAdminMembershipStatusRequestADM( + userIri, + changeSystemAdminMembershipStatusRequest, + featureFactoryConfig, + requestingUser, + apiRequestID + ) => + changeUserSystemAdminMembershipStatusADM( + userIri, + changeSystemAdminMembershipStatusRequest, + featureFactoryConfig, + requestingUser, + apiRequestID + ) case UserProjectMembershipsGetRequestADM(userIri, featureFactoryConfig, requestingUser) => userProjectMembershipsGetRequestADM(userIri, featureFactoryConfig, requestingUser) case UserProjectMembershipAddRequestADM(userIri, projectIri, featureFactoryConfig, requestingUser, apiRequestID) => userProjectMembershipAddRequestADM(userIri, projectIri, featureFactoryConfig, requestingUser, apiRequestID) - case UserProjectMembershipRemoveRequestADM(userIri, - projectIri, - featureFactoryConfig, - requestingUser, - apiRequestID) => + case UserProjectMembershipRemoveRequestADM( + userIri, + projectIri, + featureFactoryConfig, + requestingUser, + apiRequestID + ) => userProjectMembershipRemoveRequestADM(userIri, projectIri, featureFactoryConfig, requestingUser, apiRequestID) case UserProjectAdminMembershipsGetRequestADM(userIri, featureFactoryConfig, requestingUser, apiRequestID) => userProjectAdminMembershipsGetRequestADM(userIri, featureFactoryConfig, requestingUser, apiRequestID) - case UserProjectAdminMembershipAddRequestADM(userIri, - projectIri, - featureFactoryConfig, - requestingUser, - apiRequestID) => + case UserProjectAdminMembershipAddRequestADM( + userIri, + projectIri, + featureFactoryConfig, + requestingUser, + apiRequestID + ) => userProjectAdminMembershipAddRequestADM(userIri, projectIri, featureFactoryConfig, requestingUser, apiRequestID) - case UserProjectAdminMembershipRemoveRequestADM(userIri, - projectIri, - featureFactoryConfig, - requestingUser, - apiRequestID) => - userProjectAdminMembershipRemoveRequestADM(userIri, - projectIri, - featureFactoryConfig, - requestingUser, - apiRequestID) + case UserProjectAdminMembershipRemoveRequestADM( + userIri, + projectIri, + featureFactoryConfig, + requestingUser, + apiRequestID + ) => + userProjectAdminMembershipRemoveRequestADM( + userIri, + projectIri, + featureFactoryConfig, + requestingUser, + apiRequestID + ) case UserGroupMembershipsGetRequestADM(userIri, featureFactoryConfig, requestingUser) => userGroupMembershipsGetRequestADM(userIri, featureFactoryConfig, requestingUser) case UserGroupMembershipAddRequestADM(userIri, projectIri, featureFactoryConfig, requestingUser, apiRequestID) => @@ -129,1490 +154,1642 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde } /** - * Gets all the users and returns them as a sequence of [[UserADM]]. - * - * @param userInformationType the extent of the information returned. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the user initiating the request. - * @return all the users as a sequence of [[UserADM]]. - */ - private def getAllUserADM(userInformationType: UserInformationTypeADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM): Future[Seq[UserADM]] = { - - //log.debug("getAllUserADM") - + * Gets all the users and returns them as a sequence of [[UserADM]]. + * + * @param userInformationType the extent of the information returned. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the user initiating the request. + * @return all the users as a sequence of [[UserADM]]. + */ + private def getAllUserADM( + userInformationType: UserInformationTypeADM, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM + ): Future[Seq[UserADM]] = for { _ <- Future( - if (!requestingUser.permissions.isSystemAdmin && !requestingUser.permissions - .isProjectAdminInAnyProject() && !requestingUser.isSystemUser) { - throw ForbiddenException("ProjectAdmin or SystemAdmin permissions are required.") - } - ) + if ( + !requestingUser.permissions.isSystemAdmin && !requestingUser.permissions + .isProjectAdminInAnyProject() && !requestingUser.isSystemUser + ) { + throw ForbiddenException("ProjectAdmin or SystemAdmin permissions are required.") + } + ) sparqlQueryString <- Future( - org.knora.webapi.messages.twirl.queries.sparql.admin.txt - .getUsers( - triplestore = settings.triplestoreType, - maybeIri = None, - maybeUsername = None, - maybeEmail = None - ) - .toString()) + org.knora.webapi.messages.twirl.queries.sparql.admin.txt + .getUsers( + triplestore = settings.triplestoreType, + maybeIri = None, + maybeUsername = None, + maybeEmail = None + ) + .toString() + ) usersResponse <- (storeManager ? SparqlExtendedConstructRequest( - sparql = sparqlQueryString, - featureFactoryConfig = featureFactoryConfig - )).mapTo[SparqlExtendedConstructResponse] + sparql = sparqlQueryString, + featureFactoryConfig = featureFactoryConfig + )).mapTo[SparqlExtendedConstructResponse] statements = usersResponse.statements.toList - // _ = log.debug("getAllUserADM - statements: {}", statements) - - users: Seq[UserADM] = statements.map { - case (userIri: SubjectV2, propsMap: Map[SmartIri, Seq[LiteralV2]]) => - UserADM( - id = userIri.toString, - username = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.Username.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'username' defined.")) - .head - .asInstanceOf[StringLiteralV2] - .value, - email = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.Email.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'email' defined.")) - .head - .asInstanceOf[StringLiteralV2] - .value, - givenName = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.GivenName.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'givenName' defined.")) - .head - .asInstanceOf[StringLiteralV2] - .value, - familyName = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.FamilyName.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'familyName' defined.")) - .head - .asInstanceOf[StringLiteralV2] - .value, - status = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.Status.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'status' defined.")) - .head - .asInstanceOf[BooleanLiteralV2] - .value, - lang = propsMap - .getOrElse( - OntologyConstants.KnoraAdmin.PreferredLanguage.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'preferedLanguage' defined.") - ) - .head - .asInstanceOf[StringLiteralV2] - .value - ) - } + users: Seq[UserADM] = statements.map { case (userIri: SubjectV2, propsMap: Map[SmartIri, Seq[LiteralV2]]) => + UserADM( + id = userIri.toString, + username = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.Username.toSmartIri, + throw InconsistentRepositoryDataException( + s"User: $userIri has no 'username' defined." + ) + ) + .head + .asInstanceOf[StringLiteralV2] + .value, + email = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.Email.toSmartIri, + throw InconsistentRepositoryDataException(s"User: $userIri has no 'email' defined.") + ) + .head + .asInstanceOf[StringLiteralV2] + .value, + givenName = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.GivenName.toSmartIri, + throw InconsistentRepositoryDataException( + s"User: $userIri has no 'givenName' defined." + ) + ) + .head + .asInstanceOf[StringLiteralV2] + .value, + familyName = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.FamilyName.toSmartIri, + throw InconsistentRepositoryDataException( + s"User: $userIri has no 'familyName' defined." + ) + ) + .head + .asInstanceOf[StringLiteralV2] + .value, + status = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.Status.toSmartIri, + throw InconsistentRepositoryDataException( + s"User: $userIri has no 'status' defined." + ) + ) + .head + .asInstanceOf[BooleanLiteralV2] + .value, + lang = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.PreferredLanguage.toSmartIri, + throw InconsistentRepositoryDataException( + s"User: $userIri has no 'preferedLanguage' defined." + ) + ) + .head + .asInstanceOf[StringLiteralV2] + .value + ) + } } yield users.sorted - } /** - * Gets all the users and returns them as a [[UsersGetResponseADM]]. - * - * @param userInformationType the extent of the information returned. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the user initiating the request. - * @return all the users as a [[UsersGetResponseV1]]. - */ - private def getAllUserADMRequest(userInformationType: UserInformationTypeADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM): Future[UsersGetResponseADM] = { + * Gets all the users and returns them as a [[UsersGetResponseADM]]. + * + * @param userInformationType the extent of the information returned. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the user initiating the request. + * @return all the users as a [[UsersGetResponseV1]]. + */ + private def getAllUserADMRequest( + userInformationType: UserInformationTypeADM, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM + ): Future[UsersGetResponseADM] = for { maybeUsersListToReturn <- getAllUserADM( - userInformationType = userInformationType, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser - ) + userInformationType = userInformationType, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) result = maybeUsersListToReturn match { - case users: Seq[UserADM] if users.nonEmpty => - UsersGetResponseADM(users = users) - case _ => - throw NotFoundException(s"No users found") - } + case users: Seq[UserADM] if users.nonEmpty => + UsersGetResponseADM(users = users) + case _ => + throw NotFoundException(s"No users found") + } } yield result - } /** - * ~ CACHED ~ - * Gets information about a Knora user, and returns it as a [[UserADM]]. - * If possible, tries to retrieve it from the cache. If not, it retrieves - * it from the triplestore, and then writes it to the cache. Writes to the - * cache are always `UserInformationTypeADM.FULL`. - * - * @param identifier the IRI, email, or username of the user. - * @param userInformationType the type of the requested profile (restricted - * of full). - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the user initiating the request. - * @param skipCache the flag denotes to skip the cache and instead - * get data from the triplestore - * @return a [[UserADM]] describing the user. - */ - private def getSingleUserADM(identifier: UserIdentifierADM, - userInformationType: UserInformationTypeADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - skipCache: Boolean = false): Future[Option[UserADM]] = tracedFuture("admin-get-user") { - - log.debug(s"getSingleUserADM - id: {}, type: {}, requester: {}, skipCache: {}", - identifier.value, - userInformationType, - requestingUser.username, - skipCache) + * ~ CACHED ~ + * Gets information about a Knora user, and returns it as a [[UserADM]]. + * If possible, tries to retrieve it from the cache. If not, it retrieves + * it from the triplestore, and then writes it to the cache. Writes to the + * cache are always `UserInformationTypeADM.FULL`. + * + * @param identifier the IRI, email, or username of the user. + * @param userInformationType the type of the requested profile (restricted + * of full). + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the user initiating the request. + * @param skipCache the flag denotes to skip the cache and instead + * get data from the triplestore + * @return a [[UserADM]] describing the user. + */ + private def getSingleUserADM( + identifier: UserIdentifierADM, + userInformationType: UserInformationTypeADM, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + skipCache: Boolean = false + ): Future[Option[UserADM]] = tracedFuture("admin-get-user") { + + log.debug( + s"getSingleUserADM - id: {}, type: {}, requester: {}, skipCache: {}", + identifier.value, + userInformationType, + requestingUser.username, + skipCache + ) for { maybeUserADM <- if (skipCache) { - // getting directly from triplestore - getUserFromTriplestore(identifier = identifier, featureFactoryConfig = featureFactoryConfig) - } else { - // getting from cache or triplestore - getUserFromCacheOrTriplestore(identifier, featureFactoryConfig) - } + // getting directly from triplestore + getUserFromTriplestore(identifier = identifier, featureFactoryConfig = featureFactoryConfig) + } else { + // getting from cache or triplestore + getUserFromCacheOrTriplestore(identifier, featureFactoryConfig) + } // return the correct amount of information depending on either the request or user permission - finalResponse: Option[UserADM] = if (requestingUser.permissions.isSystemAdmin || requestingUser - .isSelf(identifier) || requestingUser.isSystemUser) { - // return everything or what was requested - maybeUserADM.map(user => user.ofType(userInformationType)) - } else { - // return only public information - maybeUserADM.map(user => user.ofType(UserInformationTypeADM.PUBLIC)) - } + finalResponse: Option[UserADM] = if ( + requestingUser.permissions.isSystemAdmin || requestingUser + .isSelf(identifier) || requestingUser.isSystemUser + ) { + // return everything or what was requested + maybeUserADM.map(user => user.ofType(userInformationType)) + } else { + // return only public information + maybeUserADM.map(user => user.ofType(UserInformationTypeADM.PUBLIC)) + } _ = if (finalResponse.nonEmpty) { - log.debug("getSingleUserADM - successfully retrieved user: {}", identifier.value) - } else { - log.debug("getSingleUserADM - could not retrieve user: {}", identifier.value) - } + log.debug("getSingleUserADM - successfully retrieved user: {}", identifier.value) + } else { + log.debug("getSingleUserADM - could not retrieve user: {}", identifier.value) + } } yield finalResponse } /** - * Gets information about a Knora user, and returns it as a [[UserResponseADM]]. - * - * @param identifier the IRI, username, or email of the user. - * @param userInformationType the type of the requested profile (restricted of full). - * @param requestingUser the user initiating the request. - * @return a [[UserResponseADM]] - */ - private def getSingleUserADMRequest(identifier: UserIdentifierADM, - userInformationType: UserInformationTypeADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM): Future[UserResponseADM] = { + * Gets information about a Knora user, and returns it as a [[UserResponseADM]]. + * + * @param identifier the IRI, username, or email of the user. + * @param userInformationType the type of the requested profile (restricted of full). + * @param requestingUser the user initiating the request. + * @return a [[UserResponseADM]] + */ + private def getSingleUserADMRequest( + identifier: UserIdentifierADM, + userInformationType: UserInformationTypeADM, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM + ): Future[UserResponseADM] = for { maybeUserADM <- getSingleUserADM( - identifier = identifier, - userInformationType = userInformationType, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser - ) + identifier = identifier, + userInformationType = userInformationType, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) result = maybeUserADM match { - case Some(user) => UserResponseADM(user = user) - case None => throw NotFoundException(s"User '${identifier.value}' not found") - } + case Some(user) => UserResponseADM(user = user) + case None => throw NotFoundException(s"User '${identifier.value}' not found") + } } yield result - } /** - * Updates an existing user. Only basic user data information (username, email, givenName, familyName, lang) - * can be changed. For changing the password or user status, use the separate methods. - * - * @param userIri the IRI of the existing user that we want to update. - * @param changeUserRequest the updated information. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return a future containing a [[UserOperationResponseADM]]. - * @throws BadRequestException if the necessary parameters are not supplied. - * @throws ForbiddenException if the user doesn't hold the necessary permission for the operation. - */ - private def changeBasicUserInformationADM(userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - //log.debug(s"changeBasicUserDataV1: changeUserRequest: {}", changeUserRequest) + * Updates an existing user. Only basic user data information (username, email, givenName, familyName, lang) + * can be changed. For changing the password or user status, use the separate methods. + * + * @param userIri the IRI of the existing user that we want to update. + * @param userUpdateBasicInformationPayload the updated information stored as [[UserUpdateBasicInformationPayloadADM]]. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return a future containing a [[UserOperationResponseADM]]. + * @throws BadRequestException if the necessary parameters are not supplied. + * @throws ForbiddenException if the user doesn't hold the necessary permission for the operation. + */ + private def changeBasicUserInformationADM( + userIri: IRI, + userUpdateBasicInformationPayload: UserUpdateBasicInformationPayloadADM, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { + + log.debug(s"changeBasicUserInformationADM: changeUserRequest: {}", userUpdateBasicInformationPayload) /** - * The actual change basic user data task run with an IRI lock. - */ - def changeBasicUserDataTask(userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual change basic user data task run with an IRI lock. + */ + def changeBasicUserDataTask( + userIri: IRI, + userUpdateBasicInformationPayload: UserUpdateBasicInformationPayloadADM, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - // check if the requesting user is allowed to perform updates + // check if the requesting user is allowed to perform updates (i.e. requesting updates own information or is system admin) _ <- Future( - if (!requestingUser.id.equalsIgnoreCase(userIri) && !requestingUser.permissions.isSystemAdmin) { - // not the user or a system admin - //log.debug("same user: {}, system admin: {}", userProfile.userData.user_id.contains(userIri), userProfile.permissionData.isSystemAdmin) - throw ForbiddenException( - "User information can only be changed by the user itself or a system administrator") - } - ) - - // check if necessary information is present - _ = if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") - - parametersCount = List(changeUserRequest.username, - changeUserRequest.email, - changeUserRequest.givenName, - changeUserRequest.familyName, - changeUserRequest.lang).flatten.size - _ = if (parametersCount == 0) - throw BadRequestException( - "At least one parameter needs to be supplied. No data would be changed. Aborting request for changing of basic user data.") + if (!requestingUser.id.equalsIgnoreCase(userIri) && !requestingUser.permissions.isSystemAdmin) { + throw ForbiddenException( + "User information can only be changed by the user itself or a system administrator" + ) + } + ) // get current user information currentUserInformation: Option[UserADM] <- getSingleUserADM( - identifier = UserIdentifierADM(maybeIri = Some(userIri)), - userInformationType = UserInformationTypeADM.FULL, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - ) - - // check if user exists - _ = if (currentUserInformation.isEmpty) { - throw BadRequestException(s"User ${userIri} does not exist") - } - - // check if we want to change the email - emailTaken: Boolean <- userByEmailExists(changeUserRequest.email, Some(currentUserInformation.get.email)) + identifier = UserIdentifierADM(maybeIri = Some(userIri)), + userInformationType = UserInformationTypeADM.FULL, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + ) + + // check if email is unique in case of a change email request + emailTaken: Boolean <- + userByEmailExists(userUpdateBasicInformationPayload.email, Some(currentUserInformation.get.email)) _ = if (emailTaken) { - throw DuplicateValueException(s"User with the email '${changeUserRequest.email.get}' already exists") - } + throw DuplicateValueException( + s"User with the email '${userUpdateBasicInformationPayload.email.get.value}' already exists" + ) + } - // check if we want to change the username - usernameTaken: Boolean <- userByUsernameExists(changeUserRequest.username, - Some(currentUserInformation.get.username)) + // check if username is unique in case of a change username request + usernameTaken: Boolean <- + userByUsernameExists(userUpdateBasicInformationPayload.username, Some(currentUserInformation.get.username)) _ = if (usernameTaken) { - throw DuplicateValueException(s"User with the username '${changeUserRequest.username.get}' already exists") - } - - userUpdatePayload = UserUpdatePayloadADM( - username = changeUserRequest.username, - email = changeUserRequest.email, - givenName = changeUserRequest.givenName, - familyName = changeUserRequest.familyName, - lang = changeUserRequest.lang - ) + throw DuplicateValueException( + s"User with the username '${userUpdateBasicInformationPayload.username.get.value}' already exists" + ) + } // send change request as SystemUser result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + userUpdatePayload = UserChangeRequestADM( + username = userUpdateBasicInformationPayload.username, + email = userUpdateBasicInformationPayload.email, + givenName = userUpdateBasicInformationPayload.givenName, + familyName = userUpdateBasicInformationPayload.familyName, + lang = userUpdateBasicInformationPayload.lang + ), + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) } yield result for { - // run the user update with an global IRI lock - taskResult <- IriLocker.runWithIriLock( - apiRequestID, - USERS_GLOBAL_LOCK_IRI, - () => changeBasicUserDataTask(userIri, changeUserRequest, requestingUser, apiRequestID) - ) + // run the user update with a global IRI lock + taskResult <- + IriLocker.runWithIriLock( + apiRequestID, + USERS_GLOBAL_LOCK_IRI, + () => changeBasicUserDataTask(userIri, userUpdateBasicInformationPayload, requestingUser, apiRequestID) + ) } yield taskResult } /** - * Change the users password. The old password needs to be supplied for security purposes. - * - * @param userIri the IRI of the existing user that we want to update. - * @param changeUserRequest the current password of the requesting user and the new password. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return a future containing a [[UserOperationResponseADM]]. - * @throws BadRequestException if necessary parameters are not supplied. - * @throws ForbiddenException if the user doesn't hold the necessary permission for the operation. - * @throws ForbiddenException if the supplied old password doesn't match with the user's current password. - * @throws NotFoundException if the user is not found. - */ - private def changePasswordADM(userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - log.debug(s"changePasswordADM - userIri: {}", userIri) - log.debug(s"changePasswordADM - changeUserRequest: {}", changeUserRequest) - log.debug(s"changePasswordADM - requestingUser: {}", requestingUser) + * Change the users password. The old password needs to be supplied for security purposes. + * + * @param userIri the IRI of the existing user that we want to update. + * @param userUpdatePasswordPayload the current password of the requesting user and the new password. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return a future containing a [[UserOperationResponseADM]]. + * @throws BadRequestException if necessary parameters are not supplied. + * @throws ForbiddenException if the user doesn't hold the necessary permission for the operation. + * @throws ForbiddenException if the supplied old password doesn't match with the user's current password. + * @throws NotFoundException if the user is not found. + */ + private def changePasswordADM( + userIri: IRI, + userUpdatePasswordPayload: UserUpdatePasswordPayloadADM, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { /** - * The actual change password task run with an IRI lock. - */ - def changePasswordTask(userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual change password task run with an IRI lock. + */ + def changePasswordTask( + userIri: IRI, + userUpdatePasswordPayload: UserUpdatePasswordPayloadADM, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - // check if necessary information is present - _ <- Future(if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty")) - _ = if (changeUserRequest.requesterPassword.isEmpty || changeUserRequest.newPassword.isEmpty) - throw BadRequestException("The user's old and new password need to be both supplied") - - // check if the requesting user is allowed to perform password change. it needs to be either the user himself, or a system admin - _ = if (!requestingUser.id.equalsIgnoreCase(userIri) && !requestingUser.permissions.isSystemAdmin) { - // not the user or system admin - throw ForbiddenException("User's password can only be changed by the user itself or a system admin.") - } + // check if the requesting user is allowed to perform updates (i.e. requesting updates own information or is system admin) + _ <- Future( + if (!requestingUser.id.equalsIgnoreCase(userIri) && !requestingUser.permissions.isSystemAdmin) { + throw ForbiddenException( + "User's password can only be changed by the user itself or a system administrator" + ) + } + ) // check if supplied password matches requesting user's password - _ = log.debug(s"changePasswordADM - requesterPassword: {}", changeUserRequest.requesterPassword.get) - _ = if (!requestingUser.passwordMatch(changeUserRequest.requesterPassword.get)) { - throw ForbiddenException("The supplied password does not match the requesting user's password.") - } + _ = if (!requestingUser.passwordMatch(userUpdatePasswordPayload.requesterPassword.value)) { + throw ForbiddenException("The supplied password does not match the requesting user's password.") + } - // create the update request + // hash the new password encoder = new BCryptPasswordEncoder(settings.bcryptPasswordStrength) - newHashedPassword = encoder.encode(changeUserRequest.newPassword.get) - userUpdatePayload = UserUpdatePayloadADM(password = Some(newHashedPassword)) + newHashedPassword = Password + .create(encoder.encode(userUpdatePasswordPayload.newPassword.value)) + .fold(error => throw error, value => value) // update the users password as SystemUser - result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + result <- updateUserPasswordADM( + userIri = userIri, + password = newHashedPassword, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) } yield result for { // run the change password task with an IRI lock taskResult <- IriLocker.runWithIriLock( - apiRequestID, - userIri, - () => changePasswordTask(userIri, changeUserRequest, requestingUser, apiRequestID) - ) + apiRequestID, + userIri, + () => changePasswordTask(userIri, userUpdatePasswordPayload, requestingUser, apiRequestID) + ) } yield taskResult } /** - * Change the user's status (active / inactive). - * - * @param userIri the IRI of the existing user that we want to update. - * @param changeUserRequest the new status. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return a future containing a [[UserOperationResponseADM]]. - * @throws BadRequestException if necessary parameters are not supplied. - * @throws ForbiddenException if the user doesn't hold the necessary permission for the operation. - */ - private def changeUserStatusADM(userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - log.debug(s"changeUserStatusADM - changeUserRequest: {}", changeUserRequest) + * Change the user's status (active / inactive). + * + * @param userIri the IRI of the existing user that we want to update. + * @param status the new status. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return a future containing a [[UserOperationResponseADM]]. + * @throws BadRequestException if necessary parameters are not supplied. + * @throws ForbiddenException if the user doesn't hold the necessary permission for the operation. + */ + private def changeUserStatusADM( + userIri: IRI, + status: Status, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { + + log.debug(s"changeUserStatusADM - new status: {}", status) /** - * The actual change user status task run with an IRI lock. - */ - def changeUserStatusTask(userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual change user status task run with an IRI lock. + */ + def changeUserStatusTask( + userIri: IRI, + status: Status, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - _ <- Future( - // check if necessary information is present - if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") - ) - _ = if (changeUserRequest.status.isEmpty) throw BadRequestException("New user status cannot be empty") - - // check if the requesting user is allowed to perform updates - _ = if (!requestingUser.id.equalsIgnoreCase(userIri) && !requestingUser.permissions.isSystemAdmin) { - // not the user or a system admin - // log.debug("same user: {}, system admin: {}", userProfile.userData.user_id.contains(userIri), userProfile.permissionData.isSystemAdmin) - throw ForbiddenException("User's status can only be changed by the user itself or a system administrator") - } + // check if the requesting user is allowed to perform updates (i.e. requesting updates own information or is system admin) + _ <- + Future( + if (!requestingUser.id.equalsIgnoreCase(userIri) && !requestingUser.permissions.isSystemAdmin) { + throw ForbiddenException("User's status can only be changed by the user itself or a system administrator") + } + ) // create the update request - userUpdatePayload = UserUpdatePayloadADM(status = changeUserRequest.status) - result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + userUpdatePayload = UserChangeRequestADM(status = Some(status)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) } yield result for { // run the change status task with an IRI lock taskResult <- IriLocker.runWithIriLock( - apiRequestID, - userIri, - () => changeUserStatusTask(userIri, changeUserRequest, requestingUser, apiRequestID) - ) + apiRequestID, + userIri, + () => changeUserStatusTask(userIri, status, requestingUser, apiRequestID) + ) } yield taskResult } /** - * Change the user's system admin membership status (active / inactive). - * - * @param userIri the IRI of the existing user that we want to update. - * @param changeUserRequest the new status. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the user profile of the requesting user. - * @param apiRequestID the unique api request ID. - * @return a future containing a [[UserOperationResponseADM]]. - * @throws BadRequestException if necessary parameters are not supplied. - * @throws ForbiddenException if the user doesn't hold the necessary permission for the operation. - */ - private def changeUserSystemAdminMembershipStatusADM(userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - //log.debug(s"changeUserSystemAdminMembershipStatusV1: changeUserRequest: {}", changeUserRequest) + * Change the user's system admin membership status (active / inactive). + * + * @param userIri the IRI of the existing user that we want to update. + * @param systemAdmin the new status. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the user profile of the requesting user. + * @param apiRequestID the unique api request ID. + * @return a future containing a [[UserOperationResponseADM]]. + * @throws BadRequestException if necessary parameters are not supplied. + * @throws ForbiddenException if the user doesn't hold the necessary permission for the operation. + */ + private def changeUserSystemAdminMembershipStatusADM( + userIri: IRI, + systemAdmin: SystemAdmin, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { /** - * The actual change user status task run with an IRI lock. - */ - def changeUserSystemAdminMembershipStatusTask(userIri: IRI, - changeUserRequest: ChangeUserApiRequestADM, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual change user status task run with an IRI lock. + */ + def changeUserSystemAdminMembershipStatusTask( + userIri: IRI, + systemAdmin: SystemAdmin, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - // check if necessary information is present - _ <- Future(if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty")) - _ = if (changeUserRequest.systemAdmin.isEmpty) - throw BadRequestException("New user system admin membership status cannot be empty") - - // check if the requesting user is allowed to perform updates - _ = if (!requestingUser.permissions.isSystemAdmin) { - // not a system admin - // log.debug("system admin: {}", userProfile.permissionData.isSystemAdmin) - throw ForbiddenException("User's system admin membership can only be changed by a system administrator") - } + // check if the requesting user is allowed to perform updates (i.e. system admin) + _ <- + Future( + if (!requestingUser.permissions.isSystemAdmin) { + throw ForbiddenException("User's system admin membership can only be changed by a system administrator") + } + ) // create the update request - userUpdatePayload = UserUpdatePayloadADM(systemAdmin = changeUserRequest.systemAdmin) - result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + userUpdatePayload = UserChangeRequestADM(systemAdmin = Some(systemAdmin)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) } yield result for { // run the change status task with an IRI lock - taskResult <- IriLocker.runWithIriLock( - apiRequestID, - userIri, - () => changeUserSystemAdminMembershipStatusTask(userIri, changeUserRequest, requestingUser, apiRequestID) - ) + taskResult <- + IriLocker.runWithIriLock( + apiRequestID, + userIri, + () => changeUserSystemAdminMembershipStatusTask(userIri, systemAdmin, requestingUser, apiRequestID) + ) } yield taskResult } /** - * Returns user's project memberships as a sequence of [[ProjectADM]]. - * - * @param userIri the IRI of the user. - * @param requestingUser the requesting user. - * @return a sequence of [[ProjectADM]] - */ - private def userProjectMembershipsGetADM(userIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM): Future[Seq[ProjectADM]] = { + * Returns user's project memberships as a sequence of [[ProjectADM]]. + * + * @param userIri the IRI of the user. + * @param requestingUser the requesting user. + * @return a sequence of [[ProjectADM]] + */ + private def userProjectMembershipsGetADM( + userIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM + ): Future[Seq[ProjectADM]] = for { maybeUser <- getSingleUserADM( - identifier = UserIdentifierADM(maybeIri = Some(userIri)), - userInformationType = UserInformationTypeADM.FULL, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - ) + identifier = UserIdentifierADM(maybeIri = Some(userIri)), + userInformationType = UserInformationTypeADM.FULL, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + ) result = maybeUser match { - case Some(userADM) => userADM.projects - case None => Seq.empty[ProjectADM] - } + case Some(userADM) => userADM.projects + case None => Seq.empty[ProjectADM] + } - // _ = log.debug("userProjectMembershipsGetADM - userIri: {}, projects: {}", userIri, result) } yield result - } /** - * Returns the user's project memberships as [[UserProjectMembershipsGetResponseADM]]. - * - * @param userIri the user's IRI. - * @param requestingUser the requesting user. - * @return a [[UserProjectMembershipsGetResponseADM]]. - */ + * Returns the user's project memberships as [[UserProjectMembershipsGetResponseADM]]. + * + * @param userIri the user's IRI. + * @param requestingUser the requesting user. + * @return a [[UserProjectMembershipsGetResponseADM]]. + */ private def userProjectMembershipsGetRequestADM( - userIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM): Future[UserProjectMembershipsGetResponseADM] = { - + userIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM + ): Future[UserProjectMembershipsGetResponseADM] = for { userExists <- userExists(userIri) _ = if (!userExists) { - throw BadRequestException(s"User $userIri does not exist.") - } + throw BadRequestException(s"User $userIri does not exist.") + } projects: Seq[ProjectADM] <- userProjectMembershipsGetADM( - userIri = userIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser - ) + userIri = userIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) result = UserProjectMembershipsGetResponseADM(projects = projects) } yield result - } /** - * Adds a user to a project. - * - * @param userIri the user's IRI. - * @param projectIri the project's IRI. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return - */ - private def userProjectMembershipAddRequestADM(userIri: IRI, - projectIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { + * Adds a user to a project. + * + * @param userIri the user's IRI. + * @param projectIri the project's IRI. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return + */ + private def userProjectMembershipAddRequestADM( + userIri: IRI, + projectIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { log.debug(s"userProjectMembershipAddRequestADM: userIri: {}, projectIri: {}", userIri, projectIri) /** - * The actual task run with an IRI lock. - */ - def userProjectMembershipAddRequestTask(userIri: IRI, - projectIri: IRI, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual task run with an IRI lock. + */ + def userProjectMembershipAddRequestTask( + userIri: IRI, + projectIri: IRI, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - // check if necessary information is present - _ <- Future(if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty.")) - _ = if (projectIri.isEmpty) throw BadRequestException("Project IRI cannot be empty") - - // check if the requesting user is allowed to perform updates - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { - // not a project or system admin - // log.debug("project admin: {}, system admin: {}", userProfileV1.permissionData.isProjectAdmin(projectIri), userProfileV1.permissionData.isSystemAdmin) - throw ForbiddenException("User's project membership can only be changed by a project or system administrator") - } + // check if the requesting user is allowed to perform updates (i.e. is project or system admin) + _ <- + Future( + if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { + throw ForbiddenException( + "User's project membership can only be changed by a project or system administrator" + ) + } + ) // check if user exists userExists <- userExists(userIri) - _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") + _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") // check if project exists projectExists <- projectExists(projectIri) - _ = if (!projectExists) throw NotFoundException(s"The project $projectIri does not exist.") + _ = if (!projectExists) throw NotFoundException(s"The project $projectIri does not exist.") // get users current project membership list currentProjectMemberships <- userProjectMembershipsGetRequestADM( - userIri = userIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - ) + userIri = userIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + ) currentProjectMembershipIris: Seq[IRI] = currentProjectMemberships.projects.map(_.id) // check if user is already member and if not then append to list updatedProjectMembershipIris = if (!currentProjectMembershipIris.contains(projectIri)) { - currentProjectMembershipIris :+ projectIri - } else { - throw BadRequestException(s"User $userIri is already member of project $projectIri.") - } + currentProjectMembershipIris :+ projectIri + } else { + throw BadRequestException( + s"User $userIri is already member of project $projectIri." + ) + } // create the update request - userUpdatePayload = UserUpdatePayloadADM(projects = Some(updatedProjectMembershipIris)) - result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + userUpdatePayload = UserChangeRequestADM(projects = Some(updatedProjectMembershipIris)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = apiRequestID + ) } yield result for { // run the task with an IRI lock taskResult <- IriLocker.runWithIriLock( - apiRequestID, - userIri, - () => userProjectMembershipAddRequestTask(userIri, projectIri, requestingUser, apiRequestID) - ) + apiRequestID, + userIri, + () => userProjectMembershipAddRequestTask(userIri, projectIri, requestingUser, apiRequestID) + ) } yield taskResult } /** - * Removes a user from a project. - * - * @param userIri the user's IRI. - * @param projectIri the project's IRI. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return - */ - private def userProjectMembershipRemoveRequestADM(userIri: IRI, - projectIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - // log.debug(s"userProjectMembershipRemoveRequestV1: userIri: {}, projectIri: {}", userIri, projectIri) + * Removes a user from a project. + * + * @param userIri the user's IRI. + * @param projectIri the project's IRI. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return + */ + private def userProjectMembershipRemoveRequestADM( + userIri: IRI, + projectIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { /** - * The actual task run with an IRI lock. - */ - def userProjectMembershipRemoveRequestTask(userIri: IRI, - projectIri: IRI, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual task run with an IRI lock. + */ + def userProjectMembershipRemoveRequestTask( + userIri: IRI, + projectIri: IRI, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - // check if necessary information is present - _ <- Future(if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty.")) - _ = if (projectIri.isEmpty) throw BadRequestException("Project IRI cannot be empty") - - // check if the requesting user is allowed to perform updates - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { - // not a project or system admin - // log.debug("project admin: {}, system admin: {}", userProfileV1.permissionData.isProjectAdmin(projectIri), userProfileV1.permissionData.isSystemAdmin) - throw ForbiddenException("User's project membership can only be changed by a project or system administrator") - } + // check if the requesting user is allowed to perform updates (i.e. is project or system admin) + _ <- + Future( + if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { + throw ForbiddenException( + "User's project membership can only be changed by a project or system administrator" + ) + } + ) // check if user exists userExists <- userExists(userIri) - _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") + _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") // check if project exists projectExists <- projectExists(projectIri) - _ = if (!projectExists) throw NotFoundException(s"The project $projectIri does not exist.") + _ = if (!projectExists) throw NotFoundException(s"The project $projectIri does not exist.") // get users current project membership list currentProjectMemberships <- userProjectMembershipsGetADM( - userIri = userIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - ) + userIri = userIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + ) currentProjectMembershipIris = currentProjectMemberships.map(_.id) // check if user is not already a member and if he is then remove the project from to list updatedProjectMembershipIris = if (currentProjectMembershipIris.contains(projectIri)) { - currentProjectMembershipIris diff Seq(projectIri) - } else { - throw BadRequestException(s"User $userIri is not member of project $projectIri.") - } + currentProjectMembershipIris diff Seq(projectIri) + } else { + throw BadRequestException( + s"User $userIri is not member of project $projectIri." + ) + } // create the update request by using the SystemUser - userUpdatePayload = UserUpdatePayloadADM(projects = Some(updatedProjectMembershipIris)) - result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + userUpdatePayload = UserChangeRequestADM(projects = Some(updatedProjectMembershipIris)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) } yield result for { // run the task with an IRI lock taskResult <- IriLocker.runWithIriLock( - apiRequestID, - userIri, - () => userProjectMembershipRemoveRequestTask(userIri, projectIri, requestingUser, apiRequestID) - ) + apiRequestID, + userIri, + () => userProjectMembershipRemoveRequestTask(userIri, projectIri, requestingUser, apiRequestID) + ) } yield taskResult } /** - * Returns the user's project admin group memberships as a sequence of [[IRI]] - * - * @param userIri the user's IRI. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return a [[UserProjectMembershipsGetResponseV1]]. - */ - private def userProjectAdminMembershipsGetADM(userIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[Seq[ProjectADM]] = { - + * Returns the user's project admin group memberships as a sequence of [[IRI]] + * + * @param userIri the user's IRI. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return a [[UserProjectMembershipsGetResponseV1]]. + */ + private def userProjectAdminMembershipsGetADM( + userIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[Seq[ProjectADM]] = // ToDo: only allow system user // ToDo: this is a bit of a hack since the ProjectAdmin group doesn't really exist. - for { sparqlQueryString <- Future( - org.knora.webapi.messages.twirl.queries.sparql.v1.txt - .getUserByIri( - triplestore = settings.triplestoreType, - userIri = userIri - ) - .toString()) - - //_ = log.debug("userDataByIRIGetV1 - sparqlQueryString: {}", sparqlQueryString) + org.knora.webapi.messages.twirl.queries.sparql.v1.txt + .getUserByIri( + triplestore = settings.triplestoreType, + userIri = userIri + ) + .toString() + ) userDataQueryResponse <- (storeManager ? SparqlSelectRequest(sparqlQueryString)).mapTo[SparqlSelectResult] groupedUserData: Map[String, Seq[String]] = userDataQueryResponse.results.bindings.groupBy(_.rowMap("p")).map { - case (predicate, rows) => predicate -> rows.map(_.rowMap("o")) - } + case (predicate, rows) => predicate -> rows.map(_.rowMap("o")) + } /* the projects the user is member of */ projectIris: Seq[IRI] = groupedUserData.get(OntologyConstants.KnoraAdmin.IsInProjectAdminGroup) match { - case Some(projects) => projects - case None => Seq.empty[IRI] - } + case Some(projects) => projects + case None => Seq.empty[IRI] + } maybeProjectFutures: Seq[Future[Option[ProjectADM]]] = projectIris.map { projectIri => - (responderManager ? ProjectGetADM( - identifier = ProjectIdentifierADM(maybeIri = Some(projectIri)), - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - )).mapTo[Option[ProjectADM]] - } + (responderManager ? ProjectGetADM( + identifier = + ProjectIdentifierADM(maybeIri = Some(projectIri)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + )).mapTo[Option[ProjectADM]] + } maybeProjects: Seq[Option[ProjectADM]] <- Future.sequence(maybeProjectFutures) - projects: Seq[ProjectADM] = maybeProjects.flatten + projects: Seq[ProjectADM] = maybeProjects.flatten - // _ = log.debug("userProjectAdminMembershipsGetRequestV1 - userIri: {}, projectIris: {}", userIri, projectIris) } yield projects - } /** - * Returns the user's project admin group memberships, where the result contains the IRIs of the projects the user - * is a member of the project admin group. - * - * @param userIri the user's IRI. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return a [[UserProjectMembershipsGetResponseV1]]. - */ + * Returns the user's project admin group memberships, where the result contains the IRIs of the projects the user + * is a member of the project admin group. + * + * @param userIri the user's IRI. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return a [[UserProjectMembershipsGetResponseV1]]. + */ private def userProjectAdminMembershipsGetRequestADM( - userIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserProjectAdminMembershipsGetResponseADM] = { - + userIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserProjectAdminMembershipsGetResponseADM] = // ToDo: which user is allowed to do this operation? // ToDo: check permissions - for { userExists <- userExists(userIri) _ = if (!userExists) { - throw BadRequestException(s"User $userIri does not exist.") - } + throw BadRequestException(s"User $userIri does not exist.") + } projects: Seq[ProjectADM] <- userProjectAdminMembershipsGetADM( - userIri = userIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) } yield UserProjectAdminMembershipsGetResponseADM(projects = projects) - } /** - * Adds a user to the project admin group of a project. - * - * @param userIri the user's IRI. - * @param projectIri the project's IRI. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return - */ - private def userProjectAdminMembershipAddRequestADM(userIri: IRI, - projectIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - // log.debug(s"userProjectAdminMembershipAddRequestV1: userIri: {}, projectIri: {}", userIri, projectIri) + * Adds a user to the project admin group of a project. + * + * @param userIri the user's IRI. + * @param projectIri the project's IRI. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return + */ + private def userProjectAdminMembershipAddRequestADM( + userIri: IRI, + projectIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { /** - * The actual task run with an IRI lock. - */ - def userProjectAdminMembershipAddRequestTask(userIri: IRI, - projectIri: IRI, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual task run with an IRI lock. + */ + def userProjectAdminMembershipAddRequestTask( + userIri: IRI, + projectIri: IRI, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - // check if necessary information is present - _ <- Future(if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty.")) - _ = if (projectIri.isEmpty) throw BadRequestException("Project IRI cannot be empty") - - // check if the requesting user is allowed to perform updates - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { - // not a project or system admin - // log.debug("project admin: {}, system admin: {}", userProfileV1.permissionData.isProjectAdmin(projectIri), userProfileV1.permissionData.isSystemAdmin) - throw ForbiddenException( - "User's project admin membership can only be changed by a project or system administrator") - } + // check if the requesting user is allowed to perform updates (i.e. project admin or system admin) + _ <- + Future( + if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { + throw ForbiddenException( + "User's project admin membership can only be changed by a project or system administrator" + ) + } + ) // check if user exists userExists <- userExists(userIri) - _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") + _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") // check if project exists projectExists <- projectExists(projectIri) - _ = if (!projectExists) throw NotFoundException(s"The project $projectIri does not exist.") + _ = if (!projectExists) throw NotFoundException(s"The project $projectIri does not exist.") // get users current project membership list currentProjectAdminMemberships <- userProjectAdminMembershipsGetADM( - userIri = userIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) currentProjectAdminMembershipIris: Seq[IRI] = currentProjectAdminMemberships.map(_.id) // check if user is already member and if not then append to list updatedProjectAdminMembershipIris = if (!currentProjectAdminMembershipIris.contains(projectIri)) { - currentProjectAdminMembershipIris :+ projectIri - } else { - throw BadRequestException(s"User $userIri is already a project admin for project $projectIri.") - } + currentProjectAdminMembershipIris :+ projectIri + } else { + throw BadRequestException( + s"User $userIri is already a project admin for project $projectIri." + ) + } // create the update request - userUpdatePayload = UserUpdatePayloadADM(projectsAdmin = Some(updatedProjectAdminMembershipIris)) - result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + userUpdatePayload = UserChangeRequestADM(projectsAdmin = Some(updatedProjectAdminMembershipIris)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) } yield result for { // run the task with an IRI lock taskResult <- IriLocker.runWithIriLock( - apiRequestID, - userIri, - () => userProjectAdminMembershipAddRequestTask(userIri, projectIri, requestingUser, apiRequestID) - ) + apiRequestID, + userIri, + () => userProjectAdminMembershipAddRequestTask(userIri, projectIri, requestingUser, apiRequestID) + ) } yield taskResult } /** - * Removes a user from project admin group of a project. - * - * @param userIri the user's IRI. - * @param projectIri the project's IRI. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return - */ - private def userProjectAdminMembershipRemoveRequestADM(userIri: IRI, - projectIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - // log.debug(s"userProjectAdminMembershipRemoveRequestV1: userIri: {}, projectIri: {}", userIri, projectIri) + * Removes a user from project admin group of a project. + * + * @param userIri the user's IRI. + * @param projectIri the project's IRI. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return + */ + private def userProjectAdminMembershipRemoveRequestADM( + userIri: IRI, + projectIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { /** - * The actual task run with an IRI lock. - */ - def userProjectAdminMembershipRemoveRequestTask(userIri: IRI, - projectIri: IRI, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual task run with an IRI lock. + */ + def userProjectAdminMembershipRemoveRequestTask( + userIri: IRI, + projectIri: IRI, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - // check if necessary information is present - _ <- Future(if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty.")) - _ = if (projectIri.isEmpty) throw BadRequestException("Project IRI cannot be empty") - - // check if the requesting user is allowed to perform updates - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { - // not a project or system admin - // log.debug("project admin: {}, system admin: {}", userProfileV1.permissionData.isProjectAdmin(projectIri), userProfileV1.permissionData.isSystemAdmin) - throw ForbiddenException( - "User's project admin membership can only be changed by a project or system administrator") - } + // check if the requesting user is allowed to perform updates (i.e. requesting updates own information or is system admin) + _ <- + Future( + if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { + throw ForbiddenException( + "User's project admin membership can only be changed by a project or system administrator" + ) + } + ) // check if user exists userExists <- userExists(userIri) - _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") + _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") // check if project exists projectExists <- projectExists(projectIri) - _ = if (!projectExists) throw NotFoundException(s"The project $projectIri does not exist.") + _ = if (!projectExists) throw NotFoundException(s"The project $projectIri does not exist.") // get users current project membership list currentProjectAdminMemberships <- userProjectAdminMembershipsGetADM( - userIri = userIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) currentProjectAdminMembershipIris: Seq[IRI] = currentProjectAdminMemberships.map(_.id) // check if user is not already a member and if he is then remove the project from to list updatedProjectAdminMembershipIris = if (currentProjectAdminMembershipIris.contains(projectIri)) { - currentProjectAdminMembershipIris diff Seq(projectIri) - } else { - throw BadRequestException(s"User $userIri is not a project admin of project $projectIri.") - } + currentProjectAdminMembershipIris diff Seq(projectIri) + } else { + throw BadRequestException( + s"User $userIri is not a project admin of project $projectIri." + ) + } // create the update request - userUpdatePayload = UserUpdatePayloadADM(projectsAdmin = Some(updatedProjectAdminMembershipIris)) - result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + userUpdatePayload = UserChangeRequestADM(projectsAdmin = Some(updatedProjectAdminMembershipIris)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) } yield result for { // run the task with an IRI lock - taskResult <- IriLocker.runWithIriLock( - apiRequestID, - userIri, - () => userProjectAdminMembershipRemoveRequestTask(userIri, projectIri, requestingUser, apiRequestID) - ) + taskResult <- + IriLocker.runWithIriLock( + apiRequestID, + userIri, + () => userProjectAdminMembershipRemoveRequestTask(userIri, projectIri, requestingUser, apiRequestID) + ) } yield taskResult } /** - * Returns the user's group memberships as a sequence of [[GroupADM]] - * - * @param userIri the IRI of the user. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @return a sequence of [[GroupADM]]. - */ - private def userGroupMembershipsGetADM(userIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM): Future[Seq[GroupADM]] = { - + * Returns the user's group memberships as a sequence of [[GroupADM]] + * + * @param userIri the IRI of the user. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @return a sequence of [[GroupADM]]. + */ + private def userGroupMembershipsGetADM( + userIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM + ): Future[Seq[GroupADM]] = for { maybeUserADM: Option[UserADM] <- getSingleUserADM( - identifier = UserIdentifierADM(maybeIri = Some(userIri)), - userInformationType = UserInformationTypeADM.FULL, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - ) + identifier = UserIdentifierADM(maybeIri = Some(userIri)), + userInformationType = UserInformationTypeADM.FULL, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + ) groups: Seq[GroupADM] = maybeUserADM match { - case Some(user) => - log.debug("userGroupMembershipsGetADM - user found. Returning his groups: {}.", user.groups) - user.groups - case None => - log.debug("userGroupMembershipsGetADM - user not found. Returning empty seq.") - Seq.empty[GroupADM] - } + case Some(user) => + log.debug( + "userGroupMembershipsGetADM - user found. Returning his groups: {}.", + user.groups + ) + user.groups + case None => + log.debug("userGroupMembershipsGetADM - user not found. Returning empty seq.") + Seq.empty[GroupADM] + } } yield groups - } /** - * Returns the user's group memberships as a [[UserGroupMembershipsGetResponseADM]] - * - * @param userIri the IRI of the user. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @return a [[UserGroupMembershipsGetResponseADM]]. - */ - private def userGroupMembershipsGetRequestADM(userIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM): Future[UserGroupMembershipsGetResponseADM] = { - + * Returns the user's group memberships as a [[UserGroupMembershipsGetResponseADM]] + * + * @param userIri the IRI of the user. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @return a [[UserGroupMembershipsGetResponseADM]]. + */ + private def userGroupMembershipsGetRequestADM( + userIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM + ): Future[UserGroupMembershipsGetResponseADM] = for { groups: Seq[GroupADM] <- userGroupMembershipsGetADM( - userIri = userIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - ) + userIri = userIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) } yield UserGroupMembershipsGetResponseADM(groups = groups) - } - /** - * Adds a user to a group. - * - * @param userIri the user's IRI. - * @param groupIri the group IRI. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return a [[UserOperationResponseADM]]. - */ - private def userGroupMembershipAddRequestADM(userIri: IRI, - groupIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - log.debug(s"userGroupMembershipAddRequestADM - userIri: {}, groupIri: {}", userIri, groupIri) + * Adds a user to a group. + * + * @param userIri the user's IRI. + * @param groupIri the group IRI. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return a [[UserOperationResponseADM]]. + */ + private def userGroupMembershipAddRequestADM( + userIri: IRI, + groupIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { /** - * The actual task run with an IRI lock. - */ - def userGroupMembershipAddRequestTask(userIri: IRI, - groupIri: IRI, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual task run with an IRI lock. + */ + def userGroupMembershipAddRequestTask( + userIri: IRI, + groupIri: IRI, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - // check if necessary information is present - _ <- Future(if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty.")) - _ = if (groupIri.isEmpty) throw BadRequestException("Group IRI cannot be empty") - // check if user exists maybeUser <- getSingleUserADM( - UserIdentifierADM(maybeIri = Some(userIri)), - UserInformationTypeADM.FULL, - featureFactoryConfig = featureFactoryConfig, - KnoraSystemInstances.Users.SystemUser, - skipCache = true - ) + UserIdentifierADM(maybeIri = Some(userIri)), + UserInformationTypeADM.FULL, + featureFactoryConfig = featureFactoryConfig, + KnoraSystemInstances.Users.SystemUser, + skipCache = true + ) userToChange: UserADM = maybeUser match { - case Some(user) => user - case None => throw NotFoundException(s"The user $userIri does not exist.") - } + case Some(user) => user + case None => throw NotFoundException(s"The user $userIri does not exist.") + } // check if group exists groupExists <- groupExists(groupIri) - _ = if (!groupExists) throw NotFoundException(s"The group $groupIri does not exist.") + _ = if (!groupExists) throw NotFoundException(s"The group $groupIri does not exist.") // get group's info. we need the project IRI. maybeGroupADM <- (responderManager ? GroupGetADM( - groupIri = groupIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - )).mapTo[Option[GroupADM]] + groupIri = groupIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + )).mapTo[Option[GroupADM]] + projectIri = maybeGroupADM - .getOrElse(throw InconsistentRepositoryDataException(s"Group $groupIri does not exist")) - .project - .id + .getOrElse(throw InconsistentRepositoryDataException(s"Group $groupIri does not exist")) + .project + .id - // check if the requesting user is allowed to perform updates + // check if the requesting user is allowed to perform updates (i.e. project or system administrator) _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin) { - // not a project or system admin - // log.debug("project admin: {}, system admin: {}", userProfileV1.permissionData.isProjectAdmin(projectIri), userProfileV1.permissionData.isSystemAdmin) - throw ForbiddenException("User's group membership can only be changed by a project or system administrator") - } + throw ForbiddenException( + "User's group membership can only be changed by a project or system administrator" + ) + } // get users current group membership list currentGroupMemberships = userToChange.groups currentGroupMembershipIris: Seq[IRI] = currentGroupMemberships.map(_.id) - _ = log.debug("userGroupMembershipAddRequestADM - currentGroupMembershipIris: {}", currentGroupMembershipIris) - // check if user is already member and if not then append to list updatedGroupMembershipIris = if (!currentGroupMembershipIris.contains(groupIri)) { - currentGroupMembershipIris :+ groupIri - } else { - throw BadRequestException(s"User $userIri is already member of group $groupIri.") - } - - _ = log.debug("userGroupMembershipAddRequestADM - updatedGroupMembershipIris: {}", updatedGroupMembershipIris) + currentGroupMembershipIris :+ groupIri + } else { + throw BadRequestException(s"User $userIri is already member of group $groupIri.") + } // create the update request - userUpdatePayload = UserUpdatePayloadADM(groups = Some(updatedGroupMembershipIris)) - result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + userUpdatePayload = UserChangeRequestADM(groups = Some(updatedGroupMembershipIris)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + apiRequestID = apiRequestID + ) } yield result for { // run the task with an IRI lock taskResult <- IriLocker.runWithIriLock( - apiRequestID, - userIri, - () => userGroupMembershipAddRequestTask(userIri, groupIri, requestingUser, apiRequestID) - ) + apiRequestID, + userIri, + () => userGroupMembershipAddRequestTask(userIri, groupIri, requestingUser, apiRequestID) + ) } yield taskResult } - private def userGroupMembershipRemoveRequestADM(userIri: IRI, - groupIri: IRI, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - log.debug(s"userGroupMembershipRemoveRequestADM - userIri: {}, groupIri: {}", userIri, groupIri) + private def userGroupMembershipRemoveRequestADM( + userIri: IRI, + groupIri: IRI, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { /** - * The actual task run with an IRI lock. - */ - def userGroupMembershipRemoveRequestTask(userIri: IRI, - groupIri: IRI, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = + * The actual task run with an IRI lock. + */ + def userGroupMembershipRemoveRequestTask( + userIri: IRI, + groupIri: IRI, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = for { - - // check if necessary information is present - _ <- Future(if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty.")) - _ = if (groupIri.isEmpty) throw BadRequestException("Group IRI cannot be empty") - // check if user exists userExists <- userExists(userIri) - _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") + _ = if (!userExists) throw NotFoundException(s"The user $userIri does not exist.") // check if group exists projectExists <- groupExists(groupIri) - _ = if (!projectExists) throw NotFoundException(s"The group $groupIri does not exist.") + _ = if (!projectExists) throw NotFoundException(s"The group $groupIri does not exist.") // get group's info. we need the project IRI. maybeGroupADM <- (responderManager ? GroupGetADM( - groupIri = groupIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - )).mapTo[Option[GroupADM]] + groupIri = groupIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + )).mapTo[Option[GroupADM]] projectIri = maybeGroupADM - .getOrElse(throw exceptions.InconsistentRepositoryDataException(s"Group $groupIri does not exist")) - .project - .id - - // check if the requesting user is allowed to perform updates - _ = if (!requestingUser.permissions.isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin && !requestingUser.isSystemUser) { - // not a project or system admin - //log.debug("project admin: {}, system admin: {}", userProfileV1.permissionData.isProjectAdmin(projectIri), userProfileV1.permissionData.isSystemAdmin) - throw ForbiddenException("User's group membership can only be changed by a project or system administrator") - } + .getOrElse(throw InconsistentRepositoryDataException(s"Group $groupIri does not exist")) + .project + .id + + // check if the requesting user is allowed to perform updates (i.e. is project or system admin) + _ = + if ( + !requestingUser.permissions + .isProjectAdmin(projectIri) && !requestingUser.permissions.isSystemAdmin && !requestingUser.isSystemUser + ) { + throw ForbiddenException("User's group membership can only be changed by a project or system administrator") + } // get users current project membership list currentGroupMemberships <- userGroupMembershipsGetRequestADM( - userIri = userIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - ) + userIri = userIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + ) currentGroupMembershipIris: Seq[IRI] = currentGroupMemberships.groups.map(_.id) - _ = log.debug("userGroupMembershipRemoveRequestADM - currentGroupMembershipIris: {}", - currentGroupMembershipIris) - // check if user is not already a member and if he is then remove the project from to list updatedGroupMembershipIris = if (currentGroupMembershipIris.contains(groupIri)) { - currentGroupMembershipIris diff Seq(groupIri) - } else { - throw BadRequestException(s"User $userIri is not member of group $groupIri.") - } - - _ = log.debug("userGroupMembershipRemoveRequestADM - updatedGroupMembershipIris: {}", - updatedGroupMembershipIris) + currentGroupMembershipIris diff Seq(groupIri) + } else { + throw BadRequestException(s"User $userIri is not member of group $groupIri.") + } // create the update request - userUpdatePayload = UserUpdatePayloadADM(groups = Some(updatedGroupMembershipIris)) - result <- updateUserADM( - userIri = userIri, - userUpdatePayload = userUpdatePayload, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = apiRequestID - ) + userIri = userIri, + userUpdatePayload = UserChangeRequestADM(groups = Some(updatedGroupMembershipIris)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = apiRequestID + ) } yield result for { // run the task with an IRI lock taskResult <- IriLocker.runWithIriLock( - apiRequestID, - userIri, - () => userGroupMembershipRemoveRequestTask(userIri, groupIri, requestingUser, apiRequestID) - ) + apiRequestID, + userIri, + () => userGroupMembershipRemoveRequestTask(userIri, groupIri, requestingUser, apiRequestID) + ) } yield taskResult } /** - * Updates an existing user. Should not be directly used from the receive method. - * - * @param userIri the IRI of the existing user that we want to update. - * @param userUpdatePayload the updated information. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser the requesting user. - * @param apiRequestID the unique api request ID. - * @return a future containing a [[UserOperationResponseADM]]. - * @throws BadRequestException if necessary parameters are not supplied. - * @throws UpdateNotPerformedException if the update was not performed. - */ - private def updateUserADM(userIri: IRI, - userUpdatePayload: UserUpdatePayloadADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { + * Updates an existing user. Should not be directly used from the receive method. + * + * @param userIri the IRI of the existing user that we want to update. + * @param userUpdatePayload the updated information. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return a future containing a [[UserOperationResponseADM]]. + * @throws BadRequestException if necessary parameters are not supplied. + * @throws UpdateNotPerformedException if the update was not performed. + */ + private def updateUserADM( + userIri: IRI, + userUpdatePayload: UserChangeRequestADM, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { log.debug("updateUserADM - userUpdatePayload: {}", userUpdatePayload) - /* Remember: some checks on UserUpdatePayloadV1 are implemented in the case class */ - - if (userIri.contains(KnoraSystemInstances.Users.SystemUser.id) || userIri.contains( - KnoraSystemInstances.Users.AnonymousUser.id)) { + // check if it is a request for a built-in user + if ( + userIri.contains(KnoraSystemInstances.Users.SystemUser.id) || userIri.contains( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { throw BadRequestException("Changes to built-in users are not allowed.") } for { maybeCurrentUser <- getSingleUserADM( - identifier = UserIdentifierADM(maybeIri = Some(userIri)), - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - userInformationType = UserInformationTypeADM.FULL, - skipCache = true - ) + identifier = UserIdentifierADM(maybeIri = Some(userIri)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + userInformationType = UserInformationTypeADM.FULL, + skipCache = true + ) _ = if (maybeCurrentUser.isEmpty) { - throw NotFoundException(s"User '$userIri' not found. Aborting update request.") - } + throw NotFoundException(s"User '$userIri' not found. Aborting update request.") + } + // we are changing the user, so lets get rid of the cached copy _ = invalidateCachedUserADM(maybeCurrentUser) /* Update the user */ + maybeChangedUsername = userUpdatePayload.username match { + case Some(username) => Some(username.value) + case None => None + } + maybeChangedEmail = userUpdatePayload.email match { + case Some(email) => Some(email.value) + case None => None + } + maybeChangedGivenName = userUpdatePayload.givenName match { + case Some(givenName) => + Some( + stringFormatter.toSparqlEncodedString( + givenName.value, + throw BadRequestException( + s"The supplied given name: '${givenName.value}' is not valid." + ) + ) + ) + case None => None + } + maybeChangedFamilyName = userUpdatePayload.familyName match { + case Some(familyName) => + Some( + stringFormatter.toSparqlEncodedString( + familyName.value, + throw BadRequestException( + s"The supplied family name: '${familyName.value}' is not valid." + ) + ) + ) + case None => None + } + maybeChangedStatus = userUpdatePayload.status match { + case Some(status) => Some(status.value) + case None => None + } + maybeChangedLang = userUpdatePayload.lang match { + case Some(lang) => Some(lang.value) + case None => None + } + maybeChangedProjects = userUpdatePayload.projects match { + case Some(projects) => Some(projects) + case None => None + } + maybeChangedProjectsAdmin = userUpdatePayload.projectsAdmin match { + case Some(projectsAdmin) => Some(projectsAdmin) + case None => None + } + maybeChangedGroups = userUpdatePayload.groups match { + case Some(groups) => Some(groups) + case None => None + } + maybeChangedSystemAdmin = userUpdatePayload.systemAdmin match { + case Some(systemAdmin) => Some(systemAdmin.value) + case None => None + } updateUserSparqlString <- Future( - org.knora.webapi.messages.twirl.queries.sparql.admin.txt - .updateUser( - adminNamedGraphIri = OntologyConstants.NamedGraphs.AdminNamedGraph, - triplestore = settings.triplestoreType, - userIri = userIri, - maybeUsername = userUpdatePayload.username, - maybeEmail = userUpdatePayload.email, - maybeGivenName = userUpdatePayload.givenName, - maybeFamilyName = userUpdatePayload.familyName, - maybePassword = userUpdatePayload.password, - maybeStatus = userUpdatePayload.status, - maybeLang = userUpdatePayload.lang, - maybeProjects = userUpdatePayload.projects, - maybeProjectsAdmin = userUpdatePayload.projectsAdmin, - maybeGroups = userUpdatePayload.groups, - maybeSystemAdmin = userUpdatePayload.systemAdmin - ) - .toString) - // _ = log.debug(s"updateUserV1 - query: $updateUserSparqlString") + org.knora.webapi.messages.twirl.queries.sparql.admin.txt + .updateUser( + adminNamedGraphIri = OntologyConstants.NamedGraphs.AdminNamedGraph, + triplestore = settings.triplestoreType, + userIri = userIri, + maybeUsername = maybeChangedUsername, + maybeEmail = maybeChangedEmail, + maybeGivenName = maybeChangedGivenName, + maybeFamilyName = maybeChangedFamilyName, + maybeStatus = maybeChangedStatus, + maybeLang = maybeChangedLang, + maybeProjects = maybeChangedProjects, + maybeProjectsAdmin = maybeChangedProjectsAdmin, + maybeGroups = maybeChangedGroups, + maybeSystemAdmin = maybeChangedSystemAdmin + ) + .toString + ) + updateResult <- (storeManager ? SparqlUpdateRequest(updateUserSparqlString)).mapTo[SparqlUpdateResponse] /* Verify that the user was updated. */ maybeUpdatedUserADM <- getSingleUserADM( - identifier = UserIdentifierADM(maybeIri = Some(userIri)), - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - userInformationType = UserInformationTypeADM.FULL, - skipCache = true - ) - - updatedUserADM: UserADM = maybeUpdatedUserADM.getOrElse( - throw UpdateNotPerformedException("User was not updated. Please report this as a possible bug.")) - - // _ = log.debug(s"===>>> apiUpdateRequest: $userUpdatePayload / updatedUserADM: $updatedUserADM") + identifier = UserIdentifierADM(maybeIri = Some(userIri)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + userInformationType = UserInformationTypeADM.FULL, + skipCache = true + ) + + updatedUserADM: UserADM = + maybeUpdatedUserADM.getOrElse( + throw UpdateNotPerformedException("User was not updated. Please report this as a possible bug.") + ) _ = if (userUpdatePayload.username.isDefined) { - if (updatedUserADM.username != userUpdatePayload.username.get) - throw UpdateNotPerformedException("User's 'username' was not updated. Please report this as a possible bug.") - } + if (updatedUserADM.username != userUpdatePayload.username.get.value) + throw UpdateNotPerformedException( + "User's 'username' was not updated. Please report this as a possible bug." + ) + } _ = if (userUpdatePayload.email.isDefined) { - if (updatedUserADM.email != userUpdatePayload.email.get) - throw UpdateNotPerformedException("User's 'email' was not updated. Please report this as a possible bug.") - } + if (updatedUserADM.email != userUpdatePayload.email.get.value) + throw UpdateNotPerformedException("User's 'email' was not updated. Please report this as a possible bug.") + } _ = if (userUpdatePayload.givenName.isDefined) { - if (updatedUserADM.givenName != userUpdatePayload.givenName.get) - throw UpdateNotPerformedException("User's 'givenName' was not updated. Please report this as a possible bug.") - } + if (updatedUserADM.givenName != userUpdatePayload.givenName.get.value) + throw UpdateNotPerformedException( + "User's 'givenName' was not updated. Please report this as a possible bug." + ) + } _ = if (userUpdatePayload.familyName.isDefined) { - if (updatedUserADM.familyName != userUpdatePayload.familyName.get) - throw UpdateNotPerformedException( - "User's 'familyName' was not updated. Please report this as a possible bug.") - } - - _ = if (userUpdatePayload.password.isDefined) { - if (updatedUserADM.password != userUpdatePayload.password) - throw UpdateNotPerformedException("User's 'password' was not updated. Please report this as a possible bug.") - } + if (updatedUserADM.familyName != userUpdatePayload.familyName.get.value) + throw UpdateNotPerformedException( + "User's 'familyName' was not updated. Please report this as a possible bug." + ) + } _ = if (userUpdatePayload.status.isDefined) { - if (updatedUserADM.status != userUpdatePayload.status.get) - throw UpdateNotPerformedException("User's 'status' was not updated. Please report this as a possible bug.") - } + if (updatedUserADM.status != userUpdatePayload.status.get.value) + throw UpdateNotPerformedException( + "User's 'status' was not updated. Please report this as a possible bug." + ) + } _ = if (userUpdatePayload.lang.isDefined) { - if (updatedUserADM.lang != userUpdatePayload.lang.get) - throw UpdateNotPerformedException("User's 'lang' was not updated. Please report this as a possible bug.") - } + if (updatedUserADM.lang != userUpdatePayload.lang.get.value) + throw UpdateNotPerformedException("User's 'lang' was not updated. Please report this as a possible bug.") + } + + _ = if (userUpdatePayload.projects.isDefined) { + if (updatedUserADM.projects.map(_.id) != userUpdatePayload.projects.get) + throw UpdateNotPerformedException( + "User's 'project' memberships where not updated. Please report this as a possible bug." + ) + } _ = if (userUpdatePayload.systemAdmin.isDefined) { - if (updatedUserADM.permissions.isSystemAdmin != userUpdatePayload.systemAdmin.get) - throw UpdateNotPerformedException( - "User's 'isInSystemAdminGroup' status was not updated. Please report this as a possible bug.") - } + if (updatedUserADM.permissions.isSystemAdmin != userUpdatePayload.systemAdmin.get.value) + throw UpdateNotPerformedException( + "User's 'isInSystemAdminGroup' status was not updated. Please report this as a possible bug." + ) + } _ = if (userUpdatePayload.groups.isDefined) { - if (updatedUserADM.groups.map(_.id) != userUpdatePayload.groups.get) - throw UpdateNotPerformedException( - "User's 'group' memberships where not updated. Please report this as a possible bug.") - } + if (updatedUserADM.groups.map(_.id) != userUpdatePayload.groups.get) + throw UpdateNotPerformedException( + "User's 'group' memberships where not updated. Please report this as a possible bug." + ) + } + + } yield UserOperationResponseADM(updatedUserADM.ofType(UserInformationTypeADM.RESTRICTED)) + } + + /** + * Updates the password for a user. + * + * @param userIri the IRI of the existing user that we want to update. + * @param password the new password. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser the requesting user. + * @param apiRequestID the unique api request ID. + * @return a future containing a [[UserOperationResponseADM]]. + * @throws BadRequestException if necessary parameters are not supplied. + * @throws UpdateNotPerformedException if the update was not performed. + */ + private def updateUserPasswordADM( + userIri: IRI, + password: Password, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { + + // check if it is a request for a built-in user + if ( + userIri.contains(KnoraSystemInstances.Users.SystemUser.id) || userIri.contains( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { + throw BadRequestException("Changes to built-in users are not allowed.") + } + + for { + maybeCurrentUser <- getSingleUserADM( + identifier = UserIdentifierADM(maybeIri = Some(userIri)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + userInformationType = UserInformationTypeADM.FULL, + skipCache = true + ) + + _ = if (maybeCurrentUser.isEmpty) { + throw NotFoundException(s"User '$userIri' not found. Aborting update request.") + } + // we are changing the user, so lets get rid of the cached copy + _ = invalidateCachedUserADM(maybeCurrentUser) + + // update the password + updateUserSparqlString <- Future( + org.knora.webapi.messages.twirl.queries.sparql.admin.txt + .updateUserPassword( + adminNamedGraphIri = OntologyConstants.NamedGraphs.AdminNamedGraph, + triplestore = settings.triplestoreType, + userIri = userIri, + newPassword = password.value + ) + .toString + ) + + updateResult <- (storeManager ? SparqlUpdateRequest(updateUserSparqlString)).mapTo[SparqlUpdateResponse] + + /* Verify that the password was updated. */ + maybeUpdatedUserADM <- getSingleUserADM( + identifier = UserIdentifierADM(maybeIri = Some(userIri)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + userInformationType = UserInformationTypeADM.FULL, + skipCache = true + ) + + updatedUserADM: UserADM = + maybeUpdatedUserADM.getOrElse( + throw UpdateNotPerformedException("User was not updated. Please report this as a possible bug.") + ) + + _ = if (updatedUserADM.password.get != password.value) + throw UpdateNotPerformedException("User's password was not updated. Please report this as a possible bug.") } yield UserOperationResponseADM(updatedUserADM.ofType(UserInformationTypeADM.RESTRICTED)) } /** - * Creates a new user. Self-registration is allowed, so even the default user, i.e. with no credentials supplied, - * is allowed to create a new user. - * - * Referenced Websites: - * - https://crackstation.net/hashing-security.htm - * - http://blog.ircmaxell.com/2012/12/seven-ways-to-screw-up-bcrypt.html - * - * @param createRequest a [[CreateUserApiRequestADM]] object containing information about the new user to be created. - * @param featureFactoryConfig the feature factory configuration. - * @param requestingUser a [[UserADM]] object containing information about the requesting user. - * @return a future containing the [[UserOperationResponseADM]]. - */ - private def createNewUserADM(createRequest: CreateUserApiRequestADM, - featureFactoryConfig: FeatureFactoryConfig, - requestingUser: UserADM, - apiRequestID: UUID): Future[UserOperationResponseADM] = { - - log.debug("createNewUserADM - createRequest: {}", createRequest) + * Creates a new user. Self-registration is allowed, so even the default user, i.e. with no credentials supplied, + * is allowed to create a new user. + * + * Referenced Websites: + * - https://crackstation.net/hashing-security.htm + * - http://blog.ircmaxell.com/2012/12/seven-ways-to-screw-up-bcrypt.html + * + * @param userCreatePayloadADM a [[UserCreatePayloadADM]] object containing information about the new user to be created. + * @param featureFactoryConfig the feature factory configuration. + * @param requestingUser a [[UserADM]] object containing information about the requesting user. + * @return a future containing the [[UserOperationResponseADM]]. + */ + private def createNewUserADM( + userCreatePayloadADM: UserCreatePayloadADM, + featureFactoryConfig: FeatureFactoryConfig, + requestingUser: UserADM, + apiRequestID: UUID + ): Future[UserOperationResponseADM] = { + + log.debug("createNewUserADM - userCreatePayloadADM: {}", userCreatePayloadADM) /** - * The actual task run with an IRI lock. - */ - def createNewUserTask(createRequest: CreateUserApiRequestADM, requestingUser: UserADM, apiRequestID: UUID) = + * The actual task run with an IRI lock. + */ + def createNewUserTask(userCreatePayloadADM: UserCreatePayloadADM) = for { - // check username - _ <- Future(if (createRequest.username.isEmpty) throw BadRequestException("Username cannot be empty")) - _ = stringFormatter.validateUsername( - createRequest.username, - throw BadRequestException(s"The username '${createRequest.username}' contains invalid characters")) - - // check email - _ = if (createRequest.email.isEmpty) throw BadRequestException("Email cannot be empty") - _ = stringFormatter.validateEmailAndThrow( - createRequest.email, - throw BadRequestException(s"The email '${createRequest.email}' is invalid")) - - // check other - _ = if (createRequest.password.isEmpty) throw BadRequestException("Password cannot be empty") - _ = if (createRequest.givenName.isEmpty) throw BadRequestException("Given name cannot be empty") - _ = if (createRequest.familyName.isEmpty) throw BadRequestException("Family name cannot be empty") - - usernameTaken: Boolean <- userByUsernameExists(Some(createRequest.username)) + // check if username is unique + usernameTaken: Boolean <- userByUsernameExists(userCreatePayloadADM.username) _ = if (usernameTaken) { - throw DuplicateValueException(s"User with the username '${createRequest.username}' already exists") - } + throw DuplicateValueException( + s"User with the username '${userCreatePayloadADM.username.get.value}' already exists" + ) + } - emailTaken: Boolean <- userByEmailExists(Some(createRequest.email)) + // check if email is unique + emailTaken: Boolean <- userByEmailExists(userCreatePayloadADM.email) _ = if (emailTaken) { - throw DuplicateValueException(s"User with the email '${createRequest.email}' already exists") - } + throw DuplicateValueException( + s"User with the email '${userCreatePayloadADM.email.get.value}' already exists" + ) + } // check the custom IRI; if not given, create an unused IRI - customUserIri: Option[SmartIri] = createRequest.id.map(iri => iri.toSmartIri) - userIri: IRI <- checkOrCreateEntityIri(customUserIri, stringFormatter.makeRandomPersonIri) + customUserIri: Option[SmartIri] = userCreatePayloadADM.id.map(iri => iri.toSmartIri) + userIri: IRI <- checkOrCreateEntityIri(customUserIri, stringFormatter.makeRandomPersonIri) - encoder = new BCryptPasswordEncoder(settings.bcryptPasswordStrength) - hashedPassword = encoder.encode(createRequest.password) + // hash password + encoder = new BCryptPasswordEncoder(settings.bcryptPasswordStrength) + hashedPassword = encoder.encode(userCreatePayloadADM.password.get.value) // Create the new user. createNewUserSparqlString = org.knora.webapi.messages.twirl.queries.sparql.admin.txt - .createNewUser( - adminNamedGraphIri = OntologyConstants.NamedGraphs.AdminNamedGraph, - triplestore = settings.triplestoreType, - userIri = userIri, - userClassIri = OntologyConstants.KnoraAdmin.User, - username = stringFormatter.validateAndEscapeUsername( - createRequest.username, - throw BadRequestException(s"The username '${createRequest.username}' contains invalid characters")), - email = createRequest.email, - password = hashedPassword, - givenName = createRequest.givenName, - familyName = createRequest.familyName, - status = createRequest.status, - preferredLanguage = createRequest.lang, - systemAdmin = createRequest.systemAdmin - ) - .toString - // _ = log.debug(s"createNewUser: $createNewUserSparqlString") + .createNewUser( + adminNamedGraphIri = OntologyConstants.NamedGraphs.AdminNamedGraph, + triplestore = settings.triplestoreType, + userIri = userIri, + userClassIri = OntologyConstants.KnoraAdmin.User, + username = stringFormatter.toSparqlEncodedString( + userCreatePayloadADM.username.get.value, + errorFun = throw BadRequestException( + s"The supplied username: '${userCreatePayloadADM.username.get.value}' is not valid." + ) + ), + email = stringFormatter.toSparqlEncodedString( + userCreatePayloadADM.email.get.value, + errorFun = throw BadRequestException( + s"The supplied email: '${userCreatePayloadADM.email.get.value}' is not valid." + ) + ), + password = hashedPassword, + givenName = stringFormatter.toSparqlEncodedString( + userCreatePayloadADM.givenName.get.value, + errorFun = throw BadRequestException( + s"The supplied given name: '${userCreatePayloadADM.givenName.get.value}' is not valid." + ) + ), + familyName = stringFormatter.toSparqlEncodedString( + userCreatePayloadADM.familyName.get.value, + errorFun = throw BadRequestException( + s"The supplied family name: '${userCreatePayloadADM.familyName.get.value}' is not valid." + ) + ), + status = userCreatePayloadADM.status.get.value, + preferredLanguage = stringFormatter.toSparqlEncodedString( + userCreatePayloadADM.lang.get.value, + errorFun = throw BadRequestException( + s"The supplied language: '${userCreatePayloadADM.lang.get.value}' is not valid." + ) + ), + systemAdmin = userCreatePayloadADM.systemAdmin.get.value + ) + .toString + + _ = log.debug(s"createNewUser: $createNewUserSparqlString") + createNewUserResponse <- (storeManager ? SparqlUpdateRequest(createNewUserSparqlString)) - .mapTo[SparqlUpdateResponse] + .mapTo[SparqlUpdateResponse] // try to retrieve newly created user (will also add to cache) maybeNewUserADM: Option[UserADM] <- getSingleUserADM( - identifier = UserIdentifierADM(maybeIri = Some(userIri)), - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser, - userInformationType = UserInformationTypeADM.FULL, - skipCache = true - ) + identifier = UserIdentifierADM(maybeIri = Some(userIri)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser, + userInformationType = UserInformationTypeADM.FULL, + skipCache = true + ) // check to see if we could retrieve the new user - newUserADM = maybeNewUserADM.getOrElse( - throw UpdateNotPerformedException(s"User $userIri was not created. Please report this as a possible bug.") - ) + newUserADM = + maybeNewUserADM.getOrElse( + throw UpdateNotPerformedException(s"User $userIri was not created. Please report this as a possible bug.") + ) // create the user operation response - _ = log.debug("createNewUserADM - created new user: {}", newUserADM) + _ = log.debug("createNewUserADM - created new user: {}", newUserADM) userOperationResponseADM = UserOperationResponseADM(newUserADM.ofType(UserInformationTypeADM.RESTRICTED)) } yield userOperationResponseADM - for { // run user creation with an global IRI lock taskResult <- IriLocker.runWithIriLock( - apiRequestID, - USERS_GLOBAL_LOCK_IRI, - () => createNewUserTask(createRequest, requestingUser, apiRequestID) - ) + apiRequestID, + USERS_GLOBAL_LOCK_IRI, + () => createNewUserTask(userCreatePayloadADM) + ) } yield taskResult } @@ -1621,87 +1798,92 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde //////////////////// /** - * Tries to retrieve a [[UserADM]] either from triplestore or cache if caching is enabled. - * If user is not found in cache but in triplestore, then user is written to cache. - */ - private def getUserFromCacheOrTriplestore(identifier: UserIdentifierADM, - featureFactoryConfig: FeatureFactoryConfig): Future[Option[UserADM]] = { + * Tries to retrieve a [[UserADM]] either from triplestore or cache if caching is enabled. + * If user is not found in cache but in triplestore, then user is written to cache. + */ + private def getUserFromCacheOrTriplestore( + identifier: UserIdentifierADM, + featureFactoryConfig: FeatureFactoryConfig + ): Future[Option[UserADM]] = if (cacheServiceSettings.cacheServiceEnabled) { // caching enabled - getUserFromCache(identifier) - .flatMap { - case None => - // none found in cache. getting from triplestore. - getUserFromTriplestore(identifier = identifier, featureFactoryConfig = featureFactoryConfig) - .flatMap { - case None => - // also none found in triplestore. finally returning none. - log.debug("getUserFromCacheOrTriplestore - not found in cache and in triplestore") - FastFuture.successful(None) - case Some(user) => - // found a user in the triplestore. need to write to cache. - log.debug( - "getUserFromCacheOrTriplestore - not found in cache but found in triplestore. need to write to cache.") - // writing user to cache and afterwards returning the user found in the triplestore - writeUserADMToCache(user).map(res => Some(user)) - } - case Some(user) => - log.debug("getUserFromCacheOrTriplestore - found in cache. returning user.") - FastFuture.successful(Some(user)) - } + getUserFromCache(identifier).flatMap { + case None => + // none found in cache. getting from triplestore. + getUserFromTriplestore(identifier = identifier, featureFactoryConfig = featureFactoryConfig).flatMap { + case None => + // also none found in triplestore. finally returning none. + log.debug("getUserFromCacheOrTriplestore - not found in cache and in triplestore") + FastFuture.successful(None) + case Some(user) => + // found a user in the triplestore. need to write to cache. + log.debug( + "getUserFromCacheOrTriplestore - not found in cache but found in triplestore. need to write to cache." + ) + // writing user to cache and afterwards returning the user found in the triplestore + writeUserADMToCache(user).map(res => Some(user)) + } + case Some(user) => + log.debug("getUserFromCacheOrTriplestore - found in cache. returning user.") + FastFuture.successful(Some(user)) + } } else { // caching disabled log.debug("getUserFromCacheOrTriplestore - caching disabled. getting from triplestore.") getUserFromTriplestore(identifier = identifier, featureFactoryConfig = featureFactoryConfig) } - } /** - * Tries to retrieve a [[UserADM]] from the triplestore. - */ - private def getUserFromTriplestore(identifier: UserIdentifierADM, - featureFactoryConfig: FeatureFactoryConfig): Future[Option[UserADM]] = + * Tries to retrieve a [[UserADM]] from the triplestore. + */ + private def getUserFromTriplestore( + identifier: UserIdentifierADM, + featureFactoryConfig: FeatureFactoryConfig + ): Future[Option[UserADM]] = for { sparqlQueryString <- Future( - org.knora.webapi.messages.twirl.queries.sparql.admin.txt - .getUsers( - triplestore = settings.triplestoreType, - maybeIri = identifier.toIriOption, - maybeUsername = identifier.toUsernameOption, - maybeEmail = identifier.toEmailOption - ) - .toString()) + org.knora.webapi.messages.twirl.queries.sparql.admin.txt + .getUsers( + triplestore = settings.triplestoreType, + maybeIri = identifier.toIriOption, + maybeUsername = identifier.toUsernameOption, + maybeEmail = identifier.toEmailOption + ) + .toString() + ) userQueryResponse <- (storeManager ? SparqlExtendedConstructRequest( - sparql = sparqlQueryString, - featureFactoryConfig = featureFactoryConfig - )).mapTo[SparqlExtendedConstructResponse] + sparql = sparqlQueryString, + featureFactoryConfig = featureFactoryConfig + )).mapTo[SparqlExtendedConstructResponse] maybeUserADM: Option[UserADM] <- if (userQueryResponse.statements.nonEmpty) { - log.debug("getUserFromTriplestore - triplestore hit for: {}", identifier) - statements2UserADM( - statements = userQueryResponse.statements.head, - featureFactoryConfig = featureFactoryConfig - ) - } else { - log.debug("getUserFromTriplestore - no triplestore hit for: {}", identifier) - FastFuture.successful(None) - } + log.debug("getUserFromTriplestore - triplestore hit for: {}", identifier) + statements2UserADM( + statements = userQueryResponse.statements.head, + featureFactoryConfig = featureFactoryConfig + ) + } else { + log.debug("getUserFromTriplestore - no triplestore hit for: {}", identifier) + FastFuture.successful(None) + } } yield maybeUserADM /** - * Helper method used to create a [[UserADM]] from the [[SparqlExtendedConstructResponse]] containing user data. - * - * @param statements result from the SPARQL query containing user data. - * @param featureFactoryConfig the feature factory configuration. - * @return a [[UserADM]] containing the user's data. - */ - private def statements2UserADM(statements: (SubjectV2, Map[SmartIri, Seq[LiteralV2]]), - featureFactoryConfig: FeatureFactoryConfig): Future[Option[UserADM]] = { + * Helper method used to create a [[UserADM]] from the [[SparqlExtendedConstructResponse]] containing user data. + * + * @param statements result from the SPARQL query containing user data. + * @param featureFactoryConfig the feature factory configuration. + * @return a [[UserADM]] containing the user's data. + */ + private def statements2UserADM( + statements: (SubjectV2, Map[SmartIri, Seq[LiteralV2]]), + featureFactoryConfig: FeatureFactoryConfig + ): Future[Option[UserADM]] = { // log.debug("statements2UserADM - statements: {}", statements) - val userIri: IRI = statements._1.toString + val userIri: IRI = statements._1.toString val propsMap: Map[SmartIri, Seq[LiteralV2]] = statements._2 // log.debug("statements2UserADM - userIri: {}", userIri) @@ -1737,88 +1919,99 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde for { /* get the user's permission profile from the permissions responder */ permissionData <- (responderManager ? PermissionDataGetADM( - projectIris = projectIris, - groupIris = groupIris, - isInProjectAdminGroups = isInProjectAdminGroups, - isInSystemAdminGroup = isInSystemAdminGroup, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - )).mapTo[PermissionsDataADM] + projectIris = projectIris, + groupIris = groupIris, + isInProjectAdminGroups = isInProjectAdminGroups, + isInSystemAdminGroup = isInSystemAdminGroup, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + )).mapTo[PermissionsDataADM] maybeGroupFutures: Seq[Future[Option[GroupADM]]] = groupIris.map { groupIri => - (responderManager ? GroupGetADM( - groupIri = groupIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - )).mapTo[Option[GroupADM]] - } + (responderManager ? GroupGetADM( + groupIri = groupIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = KnoraSystemInstances.Users.SystemUser + )).mapTo[Option[GroupADM]] + } maybeGroups: Seq[Option[GroupADM]] <- Future.sequence(maybeGroupFutures) - groups: Seq[GroupADM] = maybeGroups.flatten + groups: Seq[GroupADM] = maybeGroups.flatten // _ = log.debug("statements2UserADM - groups: {}", MessageUtil.toSource(groups)) maybeProjectFutures: Seq[Future[Option[ProjectADM]]] = projectIris.map { projectIri => - (responderManager ? ProjectGetADM( - ProjectIdentifierADM(maybeIri = Some(projectIri)), - featureFactoryConfig = featureFactoryConfig, - requestingUser = KnoraSystemInstances.Users.SystemUser - )).mapTo[Option[ProjectADM]] - } + (responderManager ? ProjectGetADM( + ProjectIdentifierADM(maybeIri = Some(projectIri)), + featureFactoryConfig = featureFactoryConfig, + requestingUser = + KnoraSystemInstances.Users.SystemUser + )).mapTo[Option[ProjectADM]] + } maybeProjects: Seq[Option[ProjectADM]] <- Future.sequence(maybeProjectFutures) - projects: Seq[ProjectADM] = maybeProjects.flatten + projects: Seq[ProjectADM] = maybeProjects.flatten // _ = log.debug("statements2UserADM - projects: {}", MessageUtil.toSource(projects)) /* construct the user profile from the different parts */ user = UserADM( - id = userIri, - username = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.Username.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'username' defined.")) - .head - .asInstanceOf[StringLiteralV2] - .value, - email = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.Email.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'email' defined.")) - .head - .asInstanceOf[StringLiteralV2] - .value, - password = propsMap - .get(OntologyConstants.KnoraAdmin.Password.toSmartIri) - .map(_.head.asInstanceOf[StringLiteralV2].value), - token = None, - givenName = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.GivenName.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'givenName' defined.")) - .head - .asInstanceOf[StringLiteralV2] - .value, - familyName = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.FamilyName.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'familyName' defined.")) - .head - .asInstanceOf[StringLiteralV2] - .value, - status = propsMap - .getOrElse(OntologyConstants.KnoraAdmin.Status.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'status' defined.")) - .head - .asInstanceOf[BooleanLiteralV2] - .value, - lang = propsMap - .getOrElse( - OntologyConstants.KnoraAdmin.PreferredLanguage.toSmartIri, - throw InconsistentRepositoryDataException(s"User: $userIri has no 'preferredLanguage' defined.") - ) - .head - .asInstanceOf[StringLiteralV2] - .value, - groups = groups, - projects = projects, - sessionId = None, - permissions = permissionData - ) + id = userIri, + username = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.Username.toSmartIri, + throw InconsistentRepositoryDataException(s"User: $userIri has no 'username' defined.") + ) + .head + .asInstanceOf[StringLiteralV2] + .value, + email = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.Email.toSmartIri, + throw InconsistentRepositoryDataException(s"User: $userIri has no 'email' defined.") + ) + .head + .asInstanceOf[StringLiteralV2] + .value, + password = propsMap + .get(OntologyConstants.KnoraAdmin.Password.toSmartIri) + .map(_.head.asInstanceOf[StringLiteralV2].value), + token = None, + givenName = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.GivenName.toSmartIri, + throw InconsistentRepositoryDataException(s"User: $userIri has no 'givenName' defined.") + ) + .head + .asInstanceOf[StringLiteralV2] + .value, + familyName = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.FamilyName.toSmartIri, + throw InconsistentRepositoryDataException(s"User: $userIri has no 'familyName' defined.") + ) + .head + .asInstanceOf[StringLiteralV2] + .value, + status = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.Status.toSmartIri, + throw InconsistentRepositoryDataException(s"User: $userIri has no 'status' defined.") + ) + .head + .asInstanceOf[BooleanLiteralV2] + .value, + lang = propsMap + .getOrElse( + OntologyConstants.KnoraAdmin.PreferredLanguage.toSmartIri, + throw InconsistentRepositoryDataException(s"User: $userIri has no 'preferredLanguage' defined.") + ) + .head + .asInstanceOf[StringLiteralV2] + .value, + groups = groups, + projects = projects, + sessionId = None, + permissions = permissionData + ) // _ = log.debug(s"statements2UserADM - user: {}", user.toString) result: Option[UserADM] = Some(user) @@ -1830,46 +2023,49 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde } /** - * Helper method for checking if a user exists. - * - * @param userIri the IRI of the user. - * @return a [[Boolean]]. - */ - private def userExists(userIri: IRI): Future[Boolean] = { + * Helper method for checking if a user exists. + * + * @param userIri the IRI of the user. + * @return a [[Boolean]]. + */ + private def userExists(userIri: IRI): Future[Boolean] = for { - askString <- Future( - org.knora.webapi.messages.twirl.queries.sparql.admin.txt.checkUserExists(userIri = userIri).toString) + askString <- + Future(org.knora.webapi.messages.twirl.queries.sparql.admin.txt.checkUserExists(userIri = userIri).toString) // _ = log.debug("userExists - query: {}", askString) checkUserExistsResponse <- (storeManager ? SparqlAskRequest(askString)).mapTo[SparqlAskResponse] - result = checkUserExistsResponse.result + result = checkUserExistsResponse.result } yield result - } /** - * Helper method for checking if an username is already registered. - * - * @param maybeUsername the username of the user. - * @param maybeCurrent the current username of the user. - * @return a [[Boolean]]. - */ - private def userByUsernameExists(maybeUsername: Option[String], - maybeCurrent: Option[String] = None): Future[Boolean] = { + * Helper method for checking if an username is already registered. + * + * @param maybeUsername the username of the user. + * @param maybeCurrent the current username of the user. + * @return a [[Boolean]]. + */ + private def userByUsernameExists( + maybeUsername: Option[Username], + maybeCurrent: Option[String] = None + ): Future[Boolean] = maybeUsername match { case Some(username) => - if (maybeCurrent.contains(username)) { + if (maybeCurrent.contains(username.value)) { FastFuture.successful(true) } else { stringFormatter.validateUsername( - username, - throw BadRequestException(s"The username '$username' contains invalid characters")) + username.value, + throw BadRequestException(s"The username '${username.value}' contains invalid characters") + ) for { askString <- Future( - org.knora.webapi.messages.twirl.queries.sparql.admin.txt - .checkUserExistsByUsername(username = username) - .toString) + org.knora.webapi.messages.twirl.queries.sparql.admin.txt + .checkUserExistsByUsername(username = username.value) + .toString + ) // _ = log.debug("userExists - query: {}", askString) checkUserExistsResponse <- (storeManager ? SparqlAskRequest(askString)).mapTo[SparqlAskResponse] @@ -1878,27 +2074,31 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde case None => FastFuture.successful(false) } - } /** - * Helper method for checking if an email is already registered. - * - * @param maybeEmail the email of the user. - * @param maybeCurrent the current email of the user. - * @return a [[Boolean]]. - */ - private def userByEmailExists(maybeEmail: Option[String], maybeCurrent: Option[String] = None): Future[Boolean] = { + * Helper method for checking if an email is already registered. + * + * @param maybeEmail the email of the user. + * @param maybeCurrent the current email of the user. + * @return a [[Boolean]]. + */ + private def userByEmailExists(maybeEmail: Option[Email], maybeCurrent: Option[String] = None): Future[Boolean] = maybeEmail match { case Some(email) => - if (maybeCurrent.contains(email)) { + if (maybeCurrent.contains(email.value)) { FastFuture.successful(true) } else { - stringFormatter.validateEmailAndThrow(email, - throw BadRequestException(s"The email address '$email' is invalid")) + stringFormatter.validateEmailAndThrow( + email.value, + throw BadRequestException(s"The email address '${email.value}' is invalid") + ) for { askString <- Future( - org.knora.webapi.messages.twirl.queries.sparql.admin.txt.checkUserExistsByEmail(email = email).toString) + org.knora.webapi.messages.twirl.queries.sparql.admin.txt + .checkUserExistsByEmail(email = email.value) + .toString + ) // _ = log.debug("userExists - query: {}", askString) checkUserExistsResponse <- (storeManager ? SparqlAskRequest(askString)).mapTo[SparqlAskResponse] @@ -1907,49 +2107,49 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde case None => FastFuture.successful(false) } - } /** - * Helper method for checking if a project exists. - * - * @param projectIri the IRI of the project. - * @return a [[Boolean]]. - */ - private def projectExists(projectIri: IRI): Future[Boolean] = { + * Helper method for checking if a project exists. + * + * @param projectIri the IRI of the project. + * @return a [[Boolean]]. + */ + private def projectExists(projectIri: IRI): Future[Boolean] = for { askString <- Future( - org.knora.webapi.messages.twirl.queries.sparql.admin.txt - .checkProjectExistsByIri(projectIri = projectIri) - .toString) + org.knora.webapi.messages.twirl.queries.sparql.admin.txt + .checkProjectExistsByIri(projectIri = projectIri) + .toString + ) // _ = log.debug("projectExists - query: {}", askString) checkUserExistsResponse <- (storeManager ? SparqlAskRequest(askString)).mapTo[SparqlAskResponse] - result = checkUserExistsResponse.result + result = checkUserExistsResponse.result } yield result - } /** - * Helper method for checking if a group exists. - * - * @param groupIri the IRI of the group. - * @return a [[Boolean]]. - */ - private def groupExists(groupIri: IRI): Future[Boolean] = { + * Helper method for checking if a group exists. + * + * @param groupIri the IRI of the group. + * @return a [[Boolean]]. + */ + private def groupExists(groupIri: IRI): Future[Boolean] = for { - askString <- Future( - org.knora.webapi.messages.twirl.queries.sparql.admin.txt.checkGroupExistsByIri(groupIri = groupIri).toString) + askString <- + Future( + org.knora.webapi.messages.twirl.queries.sparql.admin.txt.checkGroupExistsByIri(groupIri = groupIri).toString + ) // _ = log.debug("groupExists - query: {}", askString) checkUserExistsResponse <- (storeManager ? SparqlAskRequest(askString)).mapTo[SparqlAskResponse] - result = checkUserExistsResponse.result + result = checkUserExistsResponse.result } yield result - } /** - * Tries to retrieve a [[UserADM]] from the cache. - */ + * Tries to retrieve a [[UserADM]] from the cache. + */ private def getUserFromCache(identifier: UserIdentifierADM): Future[Option[UserADM]] = { val result = (storeManager ? CacheServiceGetUserADM(identifier)).mapTo[Option[UserADM]] result.map { @@ -1963,12 +2163,12 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde } /** - * Writes the user profile to cache. - * - * @param user a [[UserADM]]. - * @return true if writing was successful. - * @throws ApplicationCacheException when there is a problem with writing the user's profile to cache. - */ + * Writes the user profile to cache. + * + * @param user a [[UserADM]]. + * @return true if writing was successful. + * @throws ApplicationCacheException when there is a problem with writing the user's profile to cache. + */ private def writeUserADMToCache(user: UserADM): Future[Boolean] = { val result = (storeManager ? CacheServicePutUserADM(user)).mapTo[Boolean] result.map { res => @@ -1978,9 +2178,9 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde } /** - * Removes the user from cache. - */ - private def invalidateCachedUserADM(maybeUser: Option[UserADM]): Future[Boolean] = { + * Removes the user from cache. + */ + private def invalidateCachedUserADM(maybeUser: Option[UserADM]): Future[Boolean] = if (cacheServiceSettings.cacheServiceEnabled) { val keys: Set[String] = Seq(maybeUser.map(_.id), maybeUser.map(_.email), maybeUser.map(_.username)).flatten.toSet // only send to Redis if keys are not empty @@ -1999,6 +2199,4 @@ class UsersResponderADM(responderData: ResponderData) extends Responder(responde FastFuture.successful(true) } - } - } diff --git a/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala b/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala index 1a13908d3d..614de266b4 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/admin/UsersRouteADM.scala @@ -19,20 +19,19 @@ package org.knora.webapi.routing.admin -import java.util.UUID - import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{PathMatcher, Route} import io.swagger.annotations._ -import javax.ws.rs.Path import org.knora.webapi.annotation.ApiMayChange -import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.exceptions.{BadRequestException} import org.knora.webapi.feature.FeatureFactoryConfig import org.knora.webapi.messages.admin.responder.usersmessages.UsersADMJsonProtocol._ -import org.knora.webapi.messages.admin.responder.usersmessages._ +import org.knora.webapi.messages.admin.responder.usersmessages.{UserUpdatePasswordPayloadADM, _} import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.routing.{Authenticator, KnoraRoute, KnoraRouteData, RouteUtilADM} +import java.util.UUID +import javax.ws.rs.Path import scala.concurrent.Future object UsersRouteADM { @@ -40,8 +39,8 @@ object UsersRouteADM { } /** - * Provides an akka-http-routing function for API routes that deal with users. - */ + * Provides an akka-http-routing function for API routes that deal with users. + */ @Api(value = "users", produces = "application/json") @Path("/admin/users") class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) with Authenticator { @@ -49,8 +48,8 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit import UsersRouteADM._ /** - * Returns the route. - */ + * Returns the route. + */ override def makeRoute(featureFactoryConfig: FeatureFactoryConfig): Route = getUsers(featureFactoryConfig) ~ addUser(featureFactoryConfig) ~ @@ -61,7 +60,7 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit changeUserPassword(featureFactoryConfig) ~ changeUserStatus(featureFactoryConfig) ~ deleteUser(featureFactoryConfig) ~ - changeUserSytemAdminMembership(featureFactoryConfig) ~ + changeUserSystemAdminMembership(featureFactoryConfig) ~ getUsersProjectMemberships(featureFactoryConfig) ~ addUserToProjectMembership(featureFactoryConfig) ~ removeUserFromProjectMembership(featureFactoryConfig) ~ @@ -75,20 +74,20 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit @ApiResponses( Array( new ApiResponse(code = 500, message = "Internal server error") - )) + ) + ) /* return all users */ def getUsers(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath) { get { requestContext => val requestMessage: Future[UsersGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UsersGetRequestADM( - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UsersGetRequestADM( + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -101,38 +100,56 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } } - @ApiOperation(value = "Add new user", - nickname = "addUser", - httpMethod = "POST", - response = classOf[UserOperationResponseADM]) + @ApiOperation( + value = "Add new user", + nickname = "addUser", + httpMethod = "POST", + response = classOf[UserOperationResponseADM] + ) @ApiImplicitParams( Array( - new ApiImplicitParam(name = "body", - value = "\"user\" to create", - required = true, - dataTypeClass = classOf[CreateUserApiRequestADM], - paramType = "body") - )) + new ApiImplicitParam( + name = "body", + value = "\"user\" to create", + required = true, + dataTypeClass = classOf[CreateUserApiRequestADM], + paramType = "body" + ) + ) + ) @ApiResponses( Array( new ApiResponse(code = 500, message = "Internal server error") - )) + ) + ) /* create a new user */ def addUser(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath) { post { entity(as[CreateUserApiRequestADM]) { apiRequest => requestContext => + // get all values from request and make value objects from it + val user: UserCreatePayloadADM = + UserCreatePayloadADM.create( + id = stringFormatter.validateOptionalUserIri(apiRequest.id, throw BadRequestException(s"Invalid user IRI")), + username = Username.create(apiRequest.username).fold(error => throw error, value => value), + email = Email.create(apiRequest.email).fold(error => throw error, value => value), + givenName = GivenName.create(apiRequest.givenName).fold(error => throw error, value => value), + familyName = FamilyName.create(apiRequest.familyName).fold(error => throw error, value => value), + password = Password.create(apiRequest.password).fold(error => throw error, value => value), + status = Status.create(apiRequest.status).fold(error => throw error, value => value), + lang = LanguageCode.create(apiRequest.lang).fold(error => throw error, value => value), + systemAdmin = SystemAdmin.create(apiRequest.systemAdmin).fold(error => throw error, value => value) + ) val requestMessage: Future[UserCreateRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserCreateRequestADM( - createRequest = apiRequest, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserCreateRequestADM( + userCreatePayloadADM = user, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -147,23 +164,22 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * return a single user identified by iri - */ + * return a single user identified by iri + */ private def getUserByIri(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment) { - value => + userIri => get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserGetRequestADM( - identifier = UserIdentifierADM(maybeIri = Some(value)), - userInformationTypeADM = UserInformationTypeADM.RESTRICTED, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserGetRequestADM( + identifier = UserIdentifierADM(maybeIri = Some(userIri)), + userInformationTypeADM = UserInformationTypeADM.RESTRICTED, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -177,23 +193,22 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * return a single user identified by email - */ + * return a single user identified by email + */ private def getUserByEmail(featureFactoryConfig: FeatureFactoryConfig): Route = - path(UsersBasePath / "email" / Segment) { value => + path(UsersBasePath / "email" / Segment) { userIri => get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserGetRequestADM( - identifier = UserIdentifierADM(maybeEmail = Some(value)), - userInformationTypeADM = UserInformationTypeADM.RESTRICTED, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserGetRequestADM( + identifier = UserIdentifierADM(maybeEmail = Some(userIri)), + userInformationTypeADM = UserInformationTypeADM.RESTRICTED, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -207,23 +222,22 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * return a single user identified by username - */ + * return a single user identified by username + */ private def getUserByUsername(featureFactoryConfig: FeatureFactoryConfig): Route = - path(UsersBasePath / "username" / Segment) { value => + path(UsersBasePath / "username" / Segment) { userIri => get { requestContext => val requestMessage: Future[UserGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserGetRequestADM( - identifier = UserIdentifierADM(maybeUsername = Some(value)), - userInformationTypeADM = UserInformationTypeADM.RESTRICTED, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserGetRequestADM( + identifier = UserIdentifierADM(maybeUsername = Some(userIri)), + userInformationTypeADM = UserInformationTypeADM.RESTRICTED, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -237,36 +251,69 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: Change existing user's basic information. - */ + * API MAY CHANGE: Change existing user's basic information. + */ @ApiMayChange private def changeUserBasicInformation(featureFactoryConfig: FeatureFactoryConfig): Route = - path(UsersBasePath / "iri" / Segment / "BasicUserInformation") { value => + path(UsersBasePath / "iri" / Segment / "BasicUserInformation") { userIri => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => - val userIri = - stringFormatter.validateAndEscapeUserIri(value, throw BadRequestException(s"Invalid user IRI $value")) + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + + val checkedUserIri = + stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) - if (userIri.equals(KnoraSystemInstances.Users.SystemUser.id) || userIri.equals( - KnoraSystemInstances.Users.AnonymousUser.id)) { + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { throw BadRequestException("Changes to built-in users are not allowed.") } - /* the api request is already checked at time of creation. see case class. */ + val maybeChangedUsername = apiRequest.username match { + case Some(username) => Some(Username.create(username).fold(error => throw error, value => value)) + case None => None + } + val maybeChangedEmail = apiRequest.email match { + case Some(email) => Some(Email.create(email).fold(error => throw error, value => value)) + case None => None + } + val maybeChangedGivenName = apiRequest.givenName match { + case Some(givenName) => Some(GivenName.create(givenName).fold(error => throw error, value => value)) + case None => None + } + val maybeChangedFamilyName = apiRequest.familyName match { + case Some(familyName) => Some(FamilyName.create(familyName).fold(error => throw error, value => value)) + case None => None + } + val maybeChangedLang = apiRequest.lang match { + case Some(lang) => Some(LanguageCode.create(lang).fold(error => throw error, value => value)) + case None => None + } + + val userUpdatePayload: UserUpdateBasicInformationPayloadADM = + UserUpdateBasicInformationPayloadADM( + username = maybeChangedUsername, + email = maybeChangedEmail, + givenName = maybeChangedGivenName, + familyName = maybeChangedFamilyName, + lang = maybeChangedLang + ) + /* the api request is already checked at time of creation. see case class. */ val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserChangeBasicUserInformationRequestADM( - userIri = userIri, - changeUserRequest = apiRequest, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserChangeBasicInformationRequestADM( + userIri = checkedUserIri, + userUpdateBasicInformationPayload = userUpdatePayload, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -281,36 +328,47 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: Change user's password. - */ + * API MAY CHANGE: Change user's password. + */ @ApiMayChange private def changeUserPassword(featureFactoryConfig: FeatureFactoryConfig): Route = - path(UsersBasePath / "iri" / Segment / "Password") { value => + path(UsersBasePath / "iri" / Segment / "Password") { userIri => put { - entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => - val userIri = - stringFormatter.validateAndEscapeUserIri(value, throw BadRequestException(s"Invalid user IRI $value")) + entity(as[ChangeUserPasswordApiRequestADM]) { apiRequest => requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + + val checkedUserIri = + stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) - if (userIri.equals(KnoraSystemInstances.Users.SystemUser.id) || userIri.equals( - KnoraSystemInstances.Users.AnonymousUser.id)) { + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { throw BadRequestException("Changes to built-in users are not allowed.") } - /* the api request is already checked at time of creation. see case class. */ + val requesterPassword = apiRequest.requesterPassword match { + case Some(password) => Password.create(password).fold(error => throw error, value => value) + case None => throw BadRequestException("The requester's password is missing.") + } + val changedPassword = apiRequest.newPassword match { + case Some(password) => Password.create(password).fold(error => throw error, value => value) + case None => throw BadRequestException("The new password is missing.") + } val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserChangePasswordRequestADM( - userIri = userIri, - changeUserRequest = apiRequest, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserChangePasswordRequestADM( + userIri = checkedUserIri, + userUpdatePasswordPayload = UserUpdatePasswordPayloadADM(requesterPassword, changedPassword), + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -325,36 +383,43 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: Change user's status. - */ + * API MAY CHANGE: Change user's status. + */ @ApiMayChange private def changeUserStatus(featureFactoryConfig: FeatureFactoryConfig): Route = - path(UsersBasePath / "iri" / Segment / "Status") { value => + path(UsersBasePath / "iri" / Segment / "Status") { userIri => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => - val userIri = - stringFormatter.validateAndEscapeUserIri(value, throw BadRequestException(s"Invalid user IRI $value")) + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + + val checkedUserIri = + stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) - if (userIri.equals(KnoraSystemInstances.Users.SystemUser.id) || userIri.equals( - KnoraSystemInstances.Users.AnonymousUser.id)) { + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { throw BadRequestException("Changes to built-in users are not allowed.") } - /* the api request is already checked at time of creation. see case class. */ + val newStatus = apiRequest.status match { + case Some(status) => Status.create(status).fold(error => throw error, value => value) + case None => throw BadRequestException("The status is missing.") + } val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserChangeStatusRequestADM( - userIri = userIri, - changeUserRequest = apiRequest, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserChangeStatusRequestADM( + userIri = checkedUserIri, + status = newStatus, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -369,79 +434,90 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: delete a user identified by iri (change status to false). - */ + * API MAY CHANGE: delete a user identified by iri (change status to false). + */ @ApiMayChange private def deleteUser(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment) { - value => + userIri => delete { requestContext => - { - val userIri = - stringFormatter.validateAndEscapeUserIri(value, throw BadRequestException(s"Invalid user IRI $value")) + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") - if (userIri.equals(KnoraSystemInstances.Users.SystemUser.id) || userIri.equals( - KnoraSystemInstances.Users.AnonymousUser.id)) { - throw BadRequestException("Changes to built-in users are not allowed.") - } - - /* update existing user's status to false */ - val requestMessage: Future[UserChangeStatusRequestADM] = for { - requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserChangeStatusRequestADM( - userIri = userIri, - changeUserRequest = ChangeUserApiRequestADM(status = Some(false)), - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + val checkedUserIri = + stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig, - settings = settings, - responderManager = responderManager, - log = log + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id ) + ) { + throw BadRequestException("Changes to built-in users are not allowed.") } + + /* update existing user's status to false */ + val status = Status.create(false).fold(error => throw error, value => value) + + val requestMessage: Future[UserChangeStatusRequestADM] = for { + requestingUser <- getUserADM( + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserChangeStatusRequestADM( + userIri = checkedUserIri, + status = status, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) } } /** - * API MAY CHANGE: Change user's SystemAdmin membership. - */ + * API MAY CHANGE: Change user's SystemAdmin membership. + */ @ApiMayChange - private def changeUserSytemAdminMembership(featureFactoryConfig: FeatureFactoryConfig): Route = - path(UsersBasePath / "iri" / Segment / "SystemAdmin") { value => + private def changeUserSystemAdminMembership(featureFactoryConfig: FeatureFactoryConfig): Route = + path(UsersBasePath / "iri" / Segment / "SystemAdmin") { userIri => put { entity(as[ChangeUserApiRequestADM]) { apiRequest => requestContext => - val userIri = - stringFormatter.validateAndEscapeUserIri(value, throw BadRequestException(s"Invalid user IRI $value")) + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + + val checkedUserIri = + stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) - if (userIri.equals(KnoraSystemInstances.Users.SystemUser.id) || userIri.equals( - KnoraSystemInstances.Users.AnonymousUser.id)) { + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { throw BadRequestException("Changes to built-in users are not allowed.") } - /* the api request is already checked at time of creation. see case class. */ + val newSystemAdmin = apiRequest.systemAdmin match { + case Some(systemAdmin) => SystemAdmin.create(systemAdmin).fold(error => throw error, value => value) + case None => throw BadRequestException("The systemAdmin is missing.") + } val requestMessage: Future[UsersResponderRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserChangeSystemAdminMembershipStatusRequestADM( - userIri = userIri, - changeUserRequest = apiRequest, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserChangeSystemAdminMembershipStatusRequestADM( + userIri = checkedUserIri, + systemAdmin = newSystemAdmin, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -456,26 +532,27 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: get user's project memberships - */ + * API MAY CHANGE: get user's project memberships + */ @ApiMayChange private def getUsersProjectMemberships(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-memberships") { userIri => get { requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) val requestMessage: Future[UserProjectMembershipsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserProjectMembershipsGetRequestADM( - userIri = checkedUserIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserProjectMembershipsGetRequestADM( + userIri = checkedUserIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -489,31 +566,43 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: add user to project - */ + * API MAY CHANGE: add user to project + */ @ApiMayChange private def addUserToProjectMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => post { requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) + + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { + throw BadRequestException("Changes to built-in users are not allowed.") + } + val checkedProjectIri = - stringFormatter.validateAndEscapeProjectIri(projectIri, - throw BadRequestException(s"Invalid project IRI $projectIri")) + stringFormatter.validateAndEscapeProjectIri( + projectIri, + throw BadRequestException(s"Invalid project IRI $projectIri") + ) val requestMessage: Future[UserProjectMembershipAddRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserProjectMembershipAddRequestADM( - userIri = checkedUserIri, - projectIri = checkedProjectIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserProjectMembershipAddRequestADM( + userIri = checkedUserIri, + projectIri = checkedProjectIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -527,31 +616,43 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: remove user from project (and all groups belonging to this project) - */ + * API MAY CHANGE: remove user from project (and all groups belonging to this project) + */ @ApiMayChange private def removeUserFromProjectMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-memberships" / Segment) { (userIri, projectIri) => delete { requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) + + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { + throw BadRequestException("Changes to built-in users are not allowed.") + } + val checkedProjectIri = - stringFormatter.validateAndEscapeProjectIri(projectIri, - throw BadRequestException(s"Invalid project IRI $projectIri")) + stringFormatter.validateAndEscapeProjectIri( + projectIri, + throw BadRequestException(s"Invalid project IRI $projectIri") + ) val requestMessage: Future[UserProjectMembershipRemoveRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserProjectMembershipRemoveRequestADM( - userIri = checkedUserIri, - projectIri = checkedProjectIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserProjectMembershipRemoveRequestADM( + userIri = checkedUserIri, + projectIri = checkedProjectIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -565,27 +666,28 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: get user's project admin memberships - */ + * API MAY CHANGE: get user's project admin memberships + */ @ApiMayChange private def getUsersProjectAdminMemberships(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-admin-memberships") { userIri => get { requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) val requestMessage: Future[UserProjectAdminMembershipsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserProjectAdminMembershipsGetRequestADM( - userIri = checkedUserIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserProjectAdminMembershipsGetRequestADM( + userIri = checkedUserIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -599,71 +701,93 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: add user to project admin - */ + * API MAY CHANGE: add user to project admin + */ @ApiMayChange private def addUserToProjectAdminMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => - post { - /* */ - requestContext => - val checkedUserIri = - stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) - val checkedProjectIri = - stringFormatter.validateAndEscapeProjectIri(projectIri, - throw BadRequestException(s"Invalid project IRI $projectIri")) + post { requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") - val requestMessage: Future[UserProjectAdminMembershipAddRequestADM] = for { - requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserProjectAdminMembershipAddRequestADM( - userIri = checkedUserIri, - projectIri = checkedProjectIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + val checkedUserIri = + stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) - RouteUtilADM.runJsonRoute( - requestMessageF = requestMessage, - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig, - settings = settings, - responderManager = responderManager, - log = log + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id ) + ) { + throw BadRequestException("Changes to built-in users are not allowed.") + } + + val checkedProjectIri = + stringFormatter.validateAndEscapeProjectIri( + projectIri, + throw BadRequestException(s"Invalid project IRI $projectIri") + ) + + val requestMessage: Future[UserProjectAdminMembershipAddRequestADM] = for { + requestingUser <- getUserADM( + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserProjectAdminMembershipAddRequestADM( + userIri = checkedUserIri, + projectIri = checkedProjectIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) + + RouteUtilADM.runJsonRoute( + requestMessageF = requestMessage, + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig, + settings = settings, + responderManager = responderManager, + log = log + ) } } /** - * API MAY CHANGE: remove user from project admin membership - */ + * API MAY CHANGE: remove user from project admin membership + */ @ApiMayChange private def removeUserFromProjectAdminMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "project-admin-memberships" / Segment) { (userIri, projectIri) => delete { requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) + + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { + throw BadRequestException("Changes to built-in users are not allowed.") + } + val checkedProjectIri = - stringFormatter.validateAndEscapeProjectIri(projectIri, - throw BadRequestException(s"Invalid project IRI $projectIri")) + stringFormatter.validateAndEscapeProjectIri( + projectIri, + throw BadRequestException(s"Invalid project IRI $projectIri") + ) val requestMessage: Future[UserProjectAdminMembershipRemoveRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserProjectAdminMembershipRemoveRequestADM( - userIri = checkedUserIri, - projectIri = checkedProjectIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserProjectAdminMembershipRemoveRequestADM( + userIri = checkedUserIri, + projectIri = checkedProjectIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -677,26 +801,27 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: get user's group memberships - */ + * API MAY CHANGE: get user's group memberships + */ @ApiMayChange private def getUsersGroupMemberships(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "group-memberships") { userIri => get { requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) val requestMessage: Future[UserGroupMembershipsGetRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserGroupMembershipsGetRequestADM( - userIri = checkedUserIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserGroupMembershipsGetRequestADM( + userIri = checkedUserIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -710,30 +835,40 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: add user to group - */ + * API MAY CHANGE: add user to group + */ @ApiMayChange private def addUserToGroupMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => post { requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) + + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { + throw BadRequestException("Changes to built-in users are not allowed.") + } + val checkedGroupIri = stringFormatter.validateAndEscapeIri(groupIri, throw BadRequestException(s"Invalid group IRI $groupIri")) val requestMessage: Future[UserGroupMembershipAddRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserGroupMembershipAddRequestADM( - userIri = checkedUserIri, - groupIri = checkedGroupIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserGroupMembershipAddRequestADM( + userIri = checkedUserIri, + groupIri = checkedGroupIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, @@ -747,30 +882,40 @@ class UsersRouteADM(routeData: KnoraRouteData) extends KnoraRoute(routeData) wit } /** - * API MAY CHANGE: remove user from group - */ + * API MAY CHANGE: remove user from group + */ @ApiMayChange private def removeUserFromGroupMembership(featureFactoryConfig: FeatureFactoryConfig): Route = path(UsersBasePath / "iri" / Segment / "group-memberships" / Segment) { (userIri, groupIri) => delete { requestContext => + if (userIri.isEmpty) throw BadRequestException("User IRI cannot be empty") + val checkedUserIri = stringFormatter.validateAndEscapeUserIri(userIri, throw BadRequestException(s"Invalid user IRI $userIri")) + + if ( + checkedUserIri.equals(KnoraSystemInstances.Users.SystemUser.id) || checkedUserIri.equals( + KnoraSystemInstances.Users.AnonymousUser.id + ) + ) { + throw BadRequestException("Changes to built-in users are not allowed.") + } + val checkedGroupIri = stringFormatter.validateAndEscapeIri(groupIri, throw BadRequestException(s"Invalid group IRI $groupIri")) val requestMessage: Future[UserGroupMembershipRemoveRequestADM] = for { requestingUser <- getUserADM( - requestContext = requestContext, - featureFactoryConfig = featureFactoryConfig - ) - } yield - UserGroupMembershipRemoveRequestADM( - userIri = checkedUserIri, - groupIri = checkedGroupIri, - featureFactoryConfig = featureFactoryConfig, - requestingUser = requestingUser, - apiRequestID = UUID.randomUUID() - ) + requestContext = requestContext, + featureFactoryConfig = featureFactoryConfig + ) + } yield UserGroupMembershipRemoveRequestADM( + userIri = checkedUserIri, + groupIri = checkedGroupIri, + featureFactoryConfig = featureFactoryConfig, + requestingUser = requestingUser, + apiRequestID = UUID.randomUUID() + ) RouteUtilADM.runJsonRoute( requestMessageF = requestMessage, diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/updateUser.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/updateUser.scala.txt index c2f017acb2..d0a11eba8f 100644 --- a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/updateUser.scala.txt +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/updateUser.scala.txt @@ -31,7 +31,6 @@ * @param maybeEmail the new optional value for email. * @param maybeGivenName the new optional value for given name. * @param maybeFamilyName the new optional value for family name. - * @param maybePassword the new optional value for value for password. * @param maybeStatus the new optional value for status. * @param maybeLang the new optional value for lang. * @param maybeProjects the new optional value for projects. @@ -46,7 +45,6 @@ maybeEmail: Option[String], maybeGivenName: Option[String], maybeFamilyName: Option[String], - maybePassword: Option[String], maybeStatus: Option[Boolean], maybeLang: Option[String], maybeProjects: Option[Seq[IRI]], @@ -80,10 +78,6 @@ DELETE { ?user knora-admin:familyName ?currentFamilyName . } - @if(maybePassword.nonEmpty) { - ?user knora-admin:password ?currentPassword . - } - @if(maybeStatus.nonEmpty) { ?user knora-admin:status ?currentStatus . } @@ -128,10 +122,6 @@ DELETE { ?user knora-admin:familyName "@maybeFamilyName.get"^^xsd:string . } - @if(maybePassword.nonEmpty) { - ?user knora-admin:password "@maybePassword.get"^^xsd:string . - } - @if(maybeStatus.nonEmpty) { ?user knora-admin:status "@maybeStatus.get"^^xsd:boolean . } @@ -193,8 +183,6 @@ WHERE { ?user knora-admin:familyName ?currentFamilyName . - ?user knora-admin:password ?currentPassword . - ?user knora-admin:status ?currentStatus . ?user knora-admin:preferredLanguage ?currentPreferredLanguage . diff --git a/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/updateUserPassword.scala.txt b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/updateUserPassword.scala.txt new file mode 100644 index 0000000000..bb709735ec --- /dev/null +++ b/webapi/src/main/twirl/org/knora/webapi/messages/twirl/queries/sparql/admin/updateUserPassword.scala.txt @@ -0,0 +1,85 @@ +@* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + *@ + +@import org.knora.webapi.IRI +@import org.knora.webapi.messages.util.KnoraSystemInstances + +@** + * Updates an existing user with the provided values. + * + * @param dataNamedGraph the named graph to update. + * @param triplestore the name of the triplestore being used. The template uses this value to exclude inferred + results from the WHERE clause of the update. + * @param userIri the IRI of the user we want to update. + * @param newPassword the new optional value for value for password. + *@ +@(adminNamedGraphIri: IRI, + triplestore: String, + userIri: IRI, + newPassword: String) + +PREFIX rdf: +PREFIX rdfs: +PREFIX xsd: +PREFIX knora-admin: + +WITH <@adminNamedGraphIri> +DELETE { + + @* Delete current value, for which we have a new one. *@ + + @if(newPassword.nonEmpty) { + ?user knora-admin:password ?currentPassword . + } + +} INSERT { + + @* Add the new value. *@ + + @if(newPassword.nonEmpty) { + ?user knora-admin:password "@newPassword"^^xsd:string . + } + +} + +@* + +GraphDB's consistency checking requires reasoning, but reasoning interferes with certain things +in the WHERE clauses of our SPARQL updates, so we set a GraphDB-specific flag to return only +explicit statements in the WHERE clause here. + +*@ + +@triplestore match { + case "graphdb" | "graphdb-free" => { + USING + } + + case other => {} +} + +WHERE { + BIND(IRI("@userIri") AS ?user) + + @* Get all current defined values. *@ + + ?user knora-admin:password ?currentPassword . + + FILTER(!(?user = IRI("@KnoraSystemInstances.Users.AnonymousUser.id") || ?user = IRI("@KnoraSystemInstances.Users.SystemUser.id"))) +} diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/e2e/admin/BUILD.bazel index 8b4809112d..ce65f52cd8 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/BUILD.bazel +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/BUILD.bazel @@ -99,7 +99,7 @@ scala_test( scala_test( name = "UsersADME2ESpec", - size = "small", + size = "medium", srcs = [ "UsersADME2ESpec.scala", "//webapi/src/test/scala/org/knora/webapi/messages:SessionMessagesV1", diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala index 2f225113e9..e55e8edda3 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala @@ -78,35 +78,30 @@ class UsersADME2ESpec "test" ) - private val inactiveUserEmailEnc = - java.net.URLEncoder.encode(SharedTestDataV1.inactiveUser.userData.email.get, "utf-8") - private val normalUserIri = SharedTestDataV1.normalUser.userData.user_id.get private val normalUserIriEnc = java.net.URLEncoder.encode(normalUserIri, "utf-8") private val multiUserIri = SharedTestDataV1.multiuserUser.userData.user_id.get private val multiUserIriEnc = java.net.URLEncoder.encode(multiUserIri, "utf-8") - private val wrongEmail = "wrong@example.com" - private val wrongEmailEnc = java.net.URLEncoder.encode(wrongEmail, "utf-8") - - private val testPass = java.net.URLEncoder.encode("test", "utf-8") - private val wrongPass = java.net.URLEncoder.encode("wrong", "utf-8") - private val imagesProjectIri = SharedTestDataADM.imagesProject.id private val imagesProjectIriEnc = java.net.URLEncoder.encode(imagesProjectIri, "utf-8") private val imagesReviewerGroupIri = SharedTestDataADM.imagesReviewerGroup.id private val imagesReviewerGroupIriEnc = java.net.URLEncoder.encode(imagesReviewerGroupIri, "utf-8") + private val customUserIri = "http://rdfh.ch/users/prWbAoyJA7fECqhKwhSUtQ" + private val otherCustomUserIri = "http://rdfh.ch/users/prWbAoyJA7fECqhKohSUtQ" + + private val donaldIri = new MutableTestIri + private val systemUserIriEncoded = java.net.URLEncoder.encode(KnoraSystemInstances.Users.SystemUser.id, "utf-8") + // Directory path for generated client test data private val clientTestDataPath: Seq[String] = Seq("admin", "users") // Collects client test data private val clientTestDataCollector = new ClientTestDataCollector(settings) - private val customUserIri = "http://rdfh.ch/users/prWbAoyJA7fECqhKwhSUtQ" - /** * Convenience method returning the users project memberships. * @@ -176,7 +171,7 @@ class UsersADME2ESpec val request = Get(baseApiUrl + s"/admin/users/iri/${rootCreds.urlEncodedIri}") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + response.status should be(StatusCodes.OK) clientTestDataCollector.addFile( TestDataFileContent( @@ -195,7 +190,7 @@ class UsersADME2ESpec val request = Get(baseApiUrl + s"/admin/users/email/${rootCreds.urlEncodedEmail}") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + response.status should be(StatusCodes.OK) } @@ -204,7 +199,7 @@ class UsersADME2ESpec val request = Get(baseApiUrl + s"/admin/users/username/${SharedTestDataADM.rootUser.username}") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + response.status should be(StatusCodes.OK) } @@ -335,6 +330,7 @@ class UsersADME2ESpec } "given a custom Iri" should { + "create a user with the provided custom IRI " in { val createUserWithCustomIriRequest: String = s"""{ @@ -405,25 +401,91 @@ class UsersADME2ESpec errorMessage.contains(s"IRI: '$customUserIri' already exists, try another one.") invalidIri should be(true) } + } - "used to modify user information" should { + "dealing with special characters" should { - val donaldIri = new MutableTestIri + "escape special characters when creating the user" in { + val createUserWithApostropheRequest: String = + s"""{ + | "id": "$otherCustomUserIri", + | "username": "userWithApostrophe", + | "email": "userWithApostrophe@example.org", + | "givenName": "M\\"Given 'Name", + | "familyName": "M\\tFamily Name", + | "password": "test", + | "status": true, + | "lang": "en", + | "systemAdmin": false + |}""".stripMargin - "create the user if the supplied email is unique " in { + val request = Post(baseApiUrl + s"/admin/users", + HttpEntity(ContentTypes.`application/json`, createUserWithApostropheRequest)) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val result: UserADM = AkkaHttpUtils.httpResponseToJson(response).fields("user").convertTo[UserADM] + + //check that the special characters were escaped correctly + result.id should equal(otherCustomUserIri) + result.givenName should equal("M\"Given 'Name") + result.familyName should equal("M\tFamily Name") + + } + + "escape special characters when updating the user" in { + val updateUserRequest: String = + s"""{ + | "givenName": "Updated\\tGivenName", + | "familyName": "Updated\\"FamilyName" + |}""".stripMargin + + val userIriEncoded = java.net.URLEncoder.encode(otherCustomUserIri, "utf-8") + val request = Put(baseApiUrl + s"/admin/users/iri/$userIriEncoded/BasicUserInformation", + HttpEntity(ContentTypes.`application/json`, updateUserRequest)) ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val result: UserADM = AkkaHttpUtils.httpResponseToJson(response).fields("user").convertTo[UserADM] + result.givenName should be("Updated\tGivenName") + result.familyName should be("Updated\"FamilyName") + } + + "return the special characters correctly when getting a user with special characters in givenName and familyName" in { + val userIriEncoded = java.net.URLEncoder.encode(otherCustomUserIri, "utf-8") + + val request = Get(baseApiUrl + s"/admin/users/iri/$userIriEncoded") ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.OK) + + val result: UserADM = AkkaHttpUtils.httpResponseToJson(response).fields("user").convertTo[UserADM] + result.givenName should be("Updated\tGivenName") + result.familyName should be("Updated\"FamilyName") + } + + } + + "used to create a user" should { + + "create the user if the supplied email and username are unique " in { val createUserRequest: String = s"""{ - | "username": "donald.duck", - | "email": "donald.duck@example.org", - | "givenName": "Donald", - | "familyName": "Duck", - | "password": "test", - | "status": true, - | "lang": "en", - | "systemAdmin": false - |}""".stripMargin + | "username": "donald.duck", + | "email": "donald.duck@example.org", + | "givenName": "Donald", + | "familyName": "Duck", + | "password": "test", + | "status": true, + | "lang": "en", + | "systemAdmin": false + |}""".stripMargin clientTestDataCollector.addFile( TestDataFileContent( @@ -438,7 +500,6 @@ class UsersADME2ESpec val request = Post(baseApiUrl + s"/admin/users", HttpEntity(ContentTypes.`application/json`, createUserRequest)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") response.status should be(StatusCodes.OK) val result: UserADM = AkkaHttpUtils.httpResponseToJson(response).fields("user").convertTo[UserADM] @@ -450,7 +511,6 @@ class UsersADME2ESpec result.lang should be("en") donaldIri.set(result.id) - // log.debug(s"iri: ${donaldIri.get}") clientTestDataCollector.addFile( TestDataFileContent( @@ -464,13 +524,94 @@ class UsersADME2ESpec ) } + "return a 'BadRequest' if the supplied username is not unique " in { + + val createUserRequest: String = + s"""{ + | "username": "donald.duck", + | "email": "new.donald.duck@example.org", + | "givenName": "NewDonald", + | "familyName": "NewDuck", + | "password": "test", + | "status": true, + | "lang": "en", + | "systemAdmin": false + |}""".stripMargin + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "create-user-request-duplicate-username", + fileExtension = "json" + ), + text = createUserRequest + ) + ) + val request = Post(baseApiUrl + s"/admin/users", HttpEntity(ContentTypes.`application/json`, createUserRequest)) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.BadRequest) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "create-user-response-duplicate-username", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + + "return a 'BadRequest' if the supplied email is not unique " in { + + val createUserRequest: String = + s"""{ + | "username": "new.donald.duck", + | "email": "donald.duck@example.org", + | "givenName": "NewDonald", + | "familyName": "NewDuck", + | "password": "test", + | "status": true, + | "lang": "en", + | "systemAdmin": false + |}""".stripMargin + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "create-user-request-duplicate-email", + fileExtension = "json" + ), + text = createUserRequest + ) + ) + val request = Post(baseApiUrl + s"/admin/users", HttpEntity(ContentTypes.`application/json`, createUserRequest)) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.BadRequest) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "create-user-response-duplicate-email", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + "authenticate the newly created user using HttpBasicAuth" in { val request = Get(baseApiUrl + s"/v2/authentication") ~> addCredentials( BasicHttpCredentials("donald.duck@example.org", "test")) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") response.status should be(StatusCodes.OK) } @@ -487,10 +628,13 @@ class UsersADME2ESpec val request = Post(baseApiUrl + s"/v2/authentication", HttpEntity(ContentTypes.`application/json`, params)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") response.status should be(StatusCodes.OK) } + } + + "used to modify user information" should { + "update the user's basic information" in { val updateUserRequest: String = @@ -518,7 +662,6 @@ class UsersADME2ESpec HttpEntity(ContentTypes.`application/json`, updateUserRequest)) ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") response.status should be(StatusCodes.OK) val result: UserADM = AkkaHttpUtils.httpResponseToJson(response).fields("user").convertTo[UserADM] @@ -540,6 +683,64 @@ class UsersADME2ESpec ) } + "return 'BadRequest' if user IRI is None and 'NotFound' if user IRI is '' in update user request" in { + + val updateUserRequest: String = + s"""{ + | "username": "donald.without.iri.duck" + |}""".stripMargin + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "update-user-request-without-iri", + fileExtension = "json" + ), + text = updateUserRequest + ) + ) + + val missingUserIri = "" + val request = Put(baseApiUrl + s"/admin/users/iri/$missingUserIri/BasicUserInformation", + HttpEntity(ContentTypes.`application/json`, updateUserRequest)) ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val response: HttpResponse = singleAwaitingRequest(request) + + response.status should be(StatusCodes.NotFound) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "update-user-response-without-iri-1", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + + val missingUserIriNone = None + val request2 = Put(baseApiUrl + s"/admin/users/iri/$missingUserIriNone/BasicUserInformation", + HttpEntity(ContentTypes.`application/json`, updateUserRequest)) ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val response2: HttpResponse = singleAwaitingRequest(request2) + + response2.status should be(StatusCodes.BadRequest) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "update-user-response-without-iri-2", + fileExtension = "json" + ), + text = responseToString(response2) + ) + ) + + } + "update the user's password (by himself)" in { val changeUserPasswordRequest: String = @@ -609,6 +810,92 @@ class UsersADME2ESpec response2.status should be(StatusCodes.OK) } + "return 'BadRequest' if new password in change password request is missing" in { + + val changeUserPasswordRequest: String = + s"""{ + | "requesterPassword": "test" + |}""".stripMargin + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "incomplete-update-user-password-request", + fileExtension = "json" + ), + text = changeUserPasswordRequest + ) + ) + + val request1 = Put(baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/Password", + HttpEntity(ContentTypes.`application/json`, changeUserPasswordRequest)) ~> addCredentials( + BasicHttpCredentials(normalUserCreds.email, "test")) // requester's password + val response1: HttpResponse = singleAwaitingRequest(request1) + + response1.status should be(StatusCodes.BadRequest) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "incomplete-update-user-password-response", + fileExtension = "json" + ), + text = responseToString(response1) + ) + ) + + // check that the password was not changed, i.e. the old one is still accepted + val request2 = Get(baseApiUrl + s"/v2/authentication") ~> addCredentials( + BasicHttpCredentials(normalUserCreds.email, "test654321")) // old password (taken from previous test) + val response2: HttpResponse = singleAwaitingRequest(request2) + response2.status should be(StatusCodes.OK) + } + + "return 'BadRequest' if requester's password in change password request is missing" in { + + val changeUserPasswordRequest: String = + s"""{ + | "newPassword": "testABC" + |}""".stripMargin + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "incomplete-update-user-password-request-2", + fileExtension = "json" + ), + text = changeUserPasswordRequest + ) + ) + + val request1 = Put(baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/Password", + HttpEntity(ContentTypes.`application/json`, changeUserPasswordRequest)) ~> addCredentials( + BasicHttpCredentials(normalUserCreds.email, "test")) // requester's password + val response1: HttpResponse = singleAwaitingRequest(request1) + + response1.status should be(StatusCodes.BadRequest) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "incomplete-update-user-password-response-2", + fileExtension = "json" + ), + text = responseToString(response1) + ) + ) + + // check that the password was not changed, i.e. the old one is still accepted + val request2 = Get(baseApiUrl + s"/v2/authentication") ~> addCredentials( + BasicHttpCredentials(normalUserCreds.email, "test654321")) // old password + val response2: HttpResponse = singleAwaitingRequest(request2) + response2.status should be(StatusCodes.OK) + } + "change user's status" in { val changeUserStatusRequest: String = s"""{ @@ -630,7 +917,6 @@ class UsersADME2ESpec HttpEntity(ContentTypes.`application/json`, changeUserStatusRequest)) ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") response.status should be(StatusCodes.OK) val result: UserADM = AkkaHttpUtils.httpResponseToJson(response).fields("user").convertTo[UserADM] @@ -647,6 +933,23 @@ class UsersADME2ESpec ) } + "return 'BadRequest' if more than 1 parameter is provided in update status request" in { + + val updateUserRequest: String = + s"""{ + | "status": false, + | "username": "parameterDuck" + |}""".stripMargin + + val donaldIriEncoded = java.net.URLEncoder.encode(donaldIri.get, "utf-8") + val request = Put(baseApiUrl + s"/admin/users/iri/$donaldIriEncoded/Status", + HttpEntity(ContentTypes.`application/json`, updateUserRequest)) ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.BadRequest) + + } + "update the user's system admin membership status" in { val changeUserSystemAdminMembershipRequest: String = s"""{ @@ -669,14 +972,12 @@ class UsersADME2ESpec HttpEntity(ContentTypes.`application/json`, changeUserSystemAdminMembershipRequest)) ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") response.status should be(StatusCodes.OK) val result: UserADM = AkkaHttpUtils.httpResponseToJson(response).fields("user").convertTo[UserADM] result.permissions.groupsPerProject .get("http://www.knora.org/ontology/knora-admin#SystemProject") .head should equal(List("http://www.knora.org/ontology/knora-admin#SystemAdmin")) - // log.debug(jsonResult) clientTestDataCollector.addFile( TestDataFileContent( @@ -689,12 +990,30 @@ class UsersADME2ESpec ) ) + // Throw BadRequest exception if user is built-in user + val badRequest = Put( + baseApiUrl + s"/admin/users/iri/$systemUserIriEncoded/SystemAdmin", + HttpEntity(ContentTypes.`application/json`, changeUserSystemAdminMembershipRequest)) ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val badResponse: HttpResponse = singleAwaitingRequest(badRequest) + badResponse.status should be(StatusCodes.BadRequest) } - "not allow changing the system user" in { + "not allow updating the system user's system admin membership status" in { + val changeUserSystemAdminMembershipRequest: String = + s"""{ + | "systemAdmin": true + |}""".stripMargin - val systemUserIriEncoded = java.net.URLEncoder.encode(KnoraSystemInstances.Users.SystemUser.id, "utf-8") + val request = Put( + baseApiUrl + s"/admin/users/iri/$systemUserIriEncoded/SystemAdmin", + HttpEntity(ContentTypes.`application/json`, changeUserSystemAdminMembershipRequest)) ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val response: HttpResponse = singleAwaitingRequest(request) + response.status should be(StatusCodes.BadRequest) + } + "not allow changing the system user's status" in { val params = s""" { @@ -709,7 +1028,7 @@ class UsersADME2ESpec response.status should be(StatusCodes.BadRequest) } - "not allow changing the anonymous user" in { + "not allow changing the anonymous user's status" in { val anonymousUserIriEncoded = java.net.URLEncoder.encode(KnoraSystemInstances.Users.AnonymousUser.id, "utf-8") @@ -747,8 +1066,6 @@ class UsersADME2ESpec } "not allow deleting the system user" in { - val systemUserIriEncoded = java.net.URLEncoder.encode(KnoraSystemInstances.Users.SystemUser.id, "utf-8") - val request = Delete(baseApiUrl + s"/admin/users/iri/$systemUserIriEncoded") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) @@ -772,7 +1089,7 @@ class UsersADME2ESpec val request = Get(baseApiUrl + s"/admin/users/iri/$multiUserIriEnc/project-memberships") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + assert(response.status === StatusCodes.OK) val projects: Seq[ProjectADM] = @@ -797,6 +1114,7 @@ class UsersADME2ESpec } "used to modify project membership" should { + "add user to project" in { val membershipsBeforeUpdate = getUserProjectMemberships(normalUserCreds.userIri, rootCreds) membershipsBeforeUpdate should equal(Seq()) @@ -805,7 +1123,7 @@ class UsersADME2ESpec baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-memberships/$imagesProjectIriEnc") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + assert(response.status === StatusCodes.OK) val membershipsAfterUpdate = getUserProjectMemberships(normalUserIri, rootCreds) @@ -823,6 +1141,32 @@ class UsersADME2ESpec ) } + "don't add user to project if user is already a member" in { + val membershipsBeforeTryUpdate = getUserProjectMemberships(normalUserCreds.userIri, rootCreds) + + val request = Post( + baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-memberships/$imagesProjectIriEnc") ~> addCredentials( + BasicHttpCredentials(rootCreds.email, rootCreds.password)) + val response: HttpResponse = singleAwaitingRequest(request) + + assert(response.status === StatusCodes.BadRequest) + + // verify that users's project memberships weren't changed + val membershipsAfterTryUpdate = getUserProjectMemberships(normalUserIri, rootCreds) + membershipsAfterTryUpdate should equal(membershipsBeforeTryUpdate) + + clientTestDataCollector.addFile( + TestDataFileContent( + filePath = TestDataFilePath( + directoryPath = clientTestDataPath, + filename = "user-already-member-of-project-response", + fileExtension = "json" + ), + text = responseToString(response) + ) + ) + } + "remove user from project" in { val membershipsBeforeUpdate = getUserProjectMemberships(normalUserCreds.userIri, rootCreds) @@ -832,7 +1176,7 @@ class UsersADME2ESpec baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-memberships/$imagesProjectIriEnc") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + assert(response.status === StatusCodes.OK) val membershipsAfterUpdate = getUserProjectMemberships(normalUserIri, rootCreds) @@ -849,6 +1193,7 @@ class UsersADME2ESpec ) ) } + } "used to query project admin group memberships" should { @@ -857,7 +1202,6 @@ class UsersADME2ESpec val request = Get(baseApiUrl + s"/admin/users/iri/$multiUserIriEnc/project-admin-memberships") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") assert(response.status === StatusCodes.OK) val projects: Seq[ProjectADM] = @@ -879,24 +1223,24 @@ class UsersADME2ESpec ) ) } + } "used to modify project admin group membership" should { "add user to project admin group" in { val membershipsBeforeUpdate = getUserProjectAdminMemberships(normalUserCreds.userIri, rootCreds) - //log.debug(s"membershipsBeforeUpdate: $membershipsBeforeUpdate") membershipsBeforeUpdate should equal(Seq()) val request = Post( baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-admin-memberships/$imagesProjectIriEnc") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - //log.debug(s"response: ${response.toString}") + assert(response.status === StatusCodes.OK) val membershipsAfterUpdate = getUserProjectAdminMemberships(normalUserCreds.userIri, rootCreds) - //log.debug(s"membershipsAfterUpdate: $membershipsAfterUpdate") + membershipsAfterUpdate should equal(Seq(SharedTestDataADM.imagesProject)) clientTestDataCollector.addFile( @@ -914,18 +1258,18 @@ class UsersADME2ESpec "remove user from project admin group" in { val membershipsBeforeUpdate = getUserProjectAdminMemberships(normalUserCreds.userIri, rootCreds) - // log.debug(s"membershipsBeforeUpdate: $membershipsBeforeUpdate") + membershipsBeforeUpdate should equal(Seq(SharedTestDataADM.imagesProject)) val request = Delete( baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/project-admin-memberships/$imagesProjectIriEnc") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + assert(response.status === StatusCodes.OK) val membershipsAfterUpdate = getUserProjectAdminMemberships(normalUserCreds.userIri, rootCreds) - // log.debug(s"membershipsAfterUpdate: $membershipsAfterUpdate") + membershipsAfterUpdate should equal(Seq.empty[ProjectADM]) clientTestDataCollector.addFile( @@ -948,7 +1292,7 @@ class UsersADME2ESpec val request = Get(baseApiUrl + s"/admin/users/iri/$multiUserIriEnc/group-memberships") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + assert(response.status === StatusCodes.OK) val groups: Seq[GroupADM] = @@ -969,6 +1313,7 @@ class UsersADME2ESpec ) ) } + } "used to modify group membership" should { @@ -982,7 +1327,7 @@ class UsersADME2ESpec baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/group-memberships/$imagesReviewerGroupIriEnc") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + assert(response.status === StatusCodes.OK) val membershipsAfterUpdate = getUserGroupMemberships(normalUserIri, rootCreds) @@ -1009,7 +1354,7 @@ class UsersADME2ESpec baseApiUrl + s"/admin/users/iri/${normalUserCreds.urlEncodedIri}/group-memberships/$imagesReviewerGroupIriEnc") ~> addCredentials( BasicHttpCredentials(rootCreds.email, rootCreds.password)) val response: HttpResponse = singleAwaitingRequest(request) - // log.debug(s"response: ${response.toString}") + assert(response.status === StatusCodes.OK) val membershipsAfterUpdate = getUserProjectMemberships(normalUserIri, rootCreds) @@ -1026,6 +1371,8 @@ class UsersADME2ESpec ) ) } + } + } } diff --git a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/usersmessages/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/usersmessages/BUILD.bazel index bb6b88088d..599f93fb7d 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/usersmessages/BUILD.bazel +++ b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/usersmessages/BUILD.bazel @@ -21,3 +21,5 @@ scala_test( "@maven//:org_springframework_security_spring_security_core", ] + BASE_TEST_DEPENDENCIES_WITH_JSON, ) + + diff --git a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADMSpec.scala index 9e3b8efbb8..a71d484e44 100644 --- a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/usersmessages/UsersMessagesADMSpec.scala @@ -19,7 +19,7 @@ package org.knora.webapi.messages.admin.responder.usersmessages -import com.typesafe.config.ConfigFactory +import com.typesafe.config.{Config, ConfigFactory} import org.knora.webapi._ import org.knora.webapi.exceptions.BadRequestException import org.knora.webapi.messages.StringFormatter @@ -29,14 +29,14 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder object UsersMessagesADMSpec { - val config = ConfigFactory.parseString(""" + val config: Config = ConfigFactory.parseString(""" akka.loglevel = "DEBUG" akka.stdout-loglevel = "DEBUG" """.stripMargin) } /** - * This spec is used to test subclasses of the [[UsersMessagesADM]] class. + * This spec is used to test the [[UserADM]] and [[UserIdentifierADM]] classes. */ class UsersMessagesADMSpec extends CoreSpec(UsersMessagesADMSpec.config) { @@ -171,108 +171,6 @@ class UsersMessagesADMSpec extends CoreSpec(UsersMessagesADMSpec.config) { } } - "The CreateUserApiRequestADM case class" should { - - "throw 'BadRequestException' if 'username'is missing" in { - - assertThrows[BadRequestException]( - CreateUserApiRequestADM( - username = "", - email = "ddd@example.com", - givenName = "Donald", - familyName = "Duck", - password = "test", - status = true, - lang = "en", - systemAdmin = false - ) - ) - } - - "throw 'BadRequestException' if 'email' is missing" in { - - assertThrows[BadRequestException]( - CreateUserApiRequestADM( - username = "ddd", - email = "", - givenName = "Donald", - familyName = "Duck", - password = "test", - status = true, - lang = "en", - systemAdmin = false - ) - ) - } - - "throw 'BadRequestException' if 'password' is missing" in { - - assertThrows[BadRequestException]( - CreateUserApiRequestADM( - username = "donald.duck", - email = "donald.duck@example.com", - givenName = "Donald", - familyName = "Duck", - password = "", - status = true, - lang = "en", - systemAdmin = false - ) - ) - } - - "throw 'BadRequestException' if 'givenName' is missing" in { - - assertThrows[BadRequestException]( - CreateUserApiRequestADM( - username = "donald.duck", - email = "donald.duck@example.com", - givenName = "", - familyName = "Duck", - password = "test", - status = true, - lang = "en", - systemAdmin = false - ) - ) - } - - "throw 'BadRequestException' if 'familyName' is missing" in { - - assertThrows[BadRequestException]( - CreateUserApiRequestADM( - username = "donald.duck", - email = "donald.duck@example.com", - givenName = "Donald", - familyName = "", - password = "test", - status = true, - lang = "en", - systemAdmin = false - ) - ) - } - - "return 'BadRequest' if the supplied 'id' is not a valid IRI" in { - - val caught = intercept[BadRequestException]( - CreateUserApiRequestADM( - id = Some("invalid-user-IRI"), - username = "userWithInvalidCustomIri", - email = "userWithInvalidCustomIri@example.org", - givenName = "a user", - familyName = "with an invalid custom Iri", - password = "test", - status = true, - lang = "en", - systemAdmin = false - ) - ) - assert(caught.getMessage === "Invalid user IRI") - } - - } - "The UserIdentifierADM case class" should { "return the identifier type" in { @@ -334,4 +232,64 @@ class UsersMessagesADMSpec extends CoreSpec(UsersMessagesADMSpec.config) { } } + + "The ChangeUserApiRequestADM case class" should { + + "throw a BadRequestException if number of parameters is wrong" in { + + // all parameters are None + assertThrows[BadRequestException]( + ChangeUserApiRequestADM() + ) + + val errorNoParameters = the[BadRequestException] thrownBy ChangeUserApiRequestADM() + errorNoParameters.getMessage should equal("No data sent in API request.") + + // more than one parameter for status update + assertThrows[BadRequestException]( + ChangeUserApiRequestADM(status = Some(true), systemAdmin = Some(true)) + ) + + val errorTooManyParametersStatusUpdate = the[BadRequestException] thrownBy ChangeUserApiRequestADM(status = + Some(true), + systemAdmin = + Some(true)) + errorTooManyParametersStatusUpdate.getMessage should equal("Too many parameters sent for change request.") + + // more than one parameter for systemAdmin update + assertThrows[BadRequestException]( + ChangeUserApiRequestADM(systemAdmin = Some(true), status = Some(true)) + ) + + val errorTooManyParametersSystemAdminUpdate = the[BadRequestException] thrownBy ChangeUserApiRequestADM( + systemAdmin = Some(true), + status = Some(true)) + errorTooManyParametersSystemAdminUpdate.getMessage should equal("Too many parameters sent for change request.") + + // more than 5 parameters for basic user information update + assertThrows[BadRequestException]( + ChangeUserApiRequestADM( + username = Some("newUsername"), + email = Some("newEmail@email.com"), + givenName = Some("newGivenName"), + familyName = Some("familyName"), + lang = Some("en"), + status = Some(true), + systemAdmin = Some(false) + ) + ) + + val errorTooManyParametersBasicInformationUpdate = the[BadRequestException] thrownBy ChangeUserApiRequestADM( + username = Some("newUsername"), + email = Some("newEmail@email.com"), + givenName = Some("newGivenName"), + familyName = Some("familyName"), + lang = Some("en"), + status = Some(true), + systemAdmin = Some(false) + ) + errorTooManyParametersBasicInformationUpdate.getMessage should equal( + "Too many parameters sent for change request.") + } + } } diff --git a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/valueObjects/BUILD.bazel b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/valueObjects/BUILD.bazel new file mode 100644 index 0000000000..af90934728 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/valueObjects/BUILD.bazel @@ -0,0 +1,23 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_scala//scala:scala.bzl", "scala_test") +load("//third_party:dependencies.bzl", "ALL_WEBAPI_MAIN_DEPENDENCIES", "BASE_TEST_DEPENDENCIES_WITH_JSON") + +scala_test( + name = "ValueObjectsADMSpec", + size = "small", # 60s + srcs = [ + "ValueObjectsADMSpec.scala", + ], + data = [ + "//knora-ontologies", + "//test_data", + ], + jvm_flags = ["-Dconfig.resource=fuseki.conf"], + # unused_dependency_checker_mode = "warn", + deps = ALL_WEBAPI_MAIN_DEPENDENCIES + [ + "//webapi:main_library", + "//webapi:test_library", + "@maven//:org_springframework_security_spring_security_core", + ] + BASE_TEST_DEPENDENCIES_WITH_JSON, +) diff --git a/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/valueObjects/ValueObjectsADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/valueObjects/ValueObjectsADMSpec.scala new file mode 100644 index 0000000000..9edb723765 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/messages/admin/responder/valueObjects/ValueObjectsADMSpec.scala @@ -0,0 +1,230 @@ +/* + * Copyright © 2015-2021 the contributors (see Contributors.md). + * + * This file is part of Knora. + * + * Knora is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Knora is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with Knora. If not, see . + */ + +package org.knora.webapi.messages.admin.responder.valueObjects + +import com.typesafe.config.{Config, ConfigFactory} +import org.knora.webapi.exceptions.BadRequestException +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.messages.admin.responder.usersmessages._ +import org.knora.webapi.{IRI, UnitSpec} +import org.scalatest.enablers.Messaging.messagingNatureOfThrowable + +object ValueObjectsADMSpec { + val config: Config = ConfigFactory.parseString(""" + akka.loglevel = "DEBUG" + akka.stdout-loglevel = "DEBUG" + """.stripMargin) +} + +/** + * This spec is used to test the creation of value objects of the [[ValueObject]] trait. + */ +class ValueObjectsADMSpec extends UnitSpec(ValueObjectsADMSpec.config) { + + private implicit val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies + + /** + * Convenience method returning the UserCreatePayloadADM from the [[CreateUserApiRequestADM]] object + * + * @param createUserApiRequestADM the [[CreateUserApiRequestADM]] object + * @return a [[UserCreatePayloadADM]] + */ + private def createUserCreatePayloadADM(createUserApiRequestADM: CreateUserApiRequestADM): UserCreatePayloadADM = + UserCreatePayloadADM.create( + id = stringFormatter + .validateOptionalUserIri(createUserApiRequestADM.id, throw BadRequestException(s"Invalid user IRI")), + username = Username.create(createUserApiRequestADM.username).fold(error => throw error, value => value), + email = Email.create(createUserApiRequestADM.email).fold(error => throw error, value => value), + givenName = GivenName.create(createUserApiRequestADM.givenName).fold(error => throw error, value => value), + familyName = FamilyName.create(createUserApiRequestADM.familyName).fold(error => throw error, value => value), + password = Password.create(createUserApiRequestADM.password).fold(error => throw error, value => value), + status = Status.create(createUserApiRequestADM.status).fold(error => throw error, value => value), + lang = LanguageCode.create(createUserApiRequestADM.lang).fold(error => throw error, value => value), + systemAdmin = SystemAdmin.create(createUserApiRequestADM.systemAdmin).fold(error => throw error, value => value) + ) + + /** + * Convenience method returning the [[CreateUserApiRequestADM]] object + * + * @param id the optional IRI of the user to be created (unique). + * @param username the username of the user to be created (unique). + * @param email the email of the user to be created (unique). + * @param givenName the given name of the user to be created. + * @param familyName the family name of the user to be created + * @param password the password of the user to be created. + * @param status the status of the user to be created (active = true, inactive = false). + * @param lang the default language of the user to be created. + * @param systemAdmin the system admin membership. + * @return a [[UserCreatePayloadADM]] + */ + private def createUserApiRequestADM( + id: Option[IRI] = None, + username: String = "donald.duck", + email: String = "donald.duck@example.com", + givenName: String = "Donald", + familyName: String = "Duck", + password: String = "test", + status: Boolean = true, + lang: String = "en", + systemAdmin: Boolean = false + ): CreateUserApiRequestADM = + CreateUserApiRequestADM( + id = id, + username = username, + email = email, + givenName = givenName, + familyName = familyName, + password = password, + status = status, + lang = lang, + systemAdmin = systemAdmin + ) + + "When the UserCreatePayloadADM case class is created it" should { + "create a valid UserCreatePayloadADM" in { + + val request = createUserApiRequestADM() + + val userCreatePayloadADM = createUserCreatePayloadADM(request) + + userCreatePayloadADM.id should equal(request.id) + userCreatePayloadADM.username.get.value should equal(request.username) + userCreatePayloadADM.email.get.value should equal(request.email) + userCreatePayloadADM.password.get.value should equal(request.password) + userCreatePayloadADM.givenName.get.value should equal(request.givenName) + userCreatePayloadADM.familyName.get.value should equal(request.familyName) + userCreatePayloadADM.status.get.value should equal(request.status) + userCreatePayloadADM.lang.get.value should equal(request.lang) + userCreatePayloadADM.systemAdmin.get.value should equal(request.systemAdmin) + + val otherRequest = createUserApiRequestADM( + id = Some("http://rdfh.ch/users/notdonald"), + username = "not.donald.duck", + email = "not.donald.duck@example.com", + givenName = "NotDonald", + familyName = "NotDuck", + password = "notDonaldDuckTest", + status = false, + lang = "de", + systemAdmin = true + ) + + val otherUserCreatePayloadADM = createUserCreatePayloadADM(otherRequest) + + otherUserCreatePayloadADM.id should equal(otherRequest.id) + otherUserCreatePayloadADM.username.get.value should equal(otherRequest.username) + otherUserCreatePayloadADM.email.get.value should equal(otherRequest.email) + otherUserCreatePayloadADM.password.get.value should equal(otherRequest.password) + otherUserCreatePayloadADM.givenName.get.value should equal(otherRequest.givenName) + otherUserCreatePayloadADM.familyName.get.value should equal(otherRequest.familyName) + otherUserCreatePayloadADM.status.get.value should equal(otherRequest.status) + otherUserCreatePayloadADM.lang.get.value should equal(otherRequest.lang) + otherUserCreatePayloadADM.systemAdmin.get.value should equal(otherRequest.systemAdmin) + + otherUserCreatePayloadADM.id should not equal request.id + otherUserCreatePayloadADM.username.get.value should not equal request.username + otherUserCreatePayloadADM.email.get.value should not equal request.email + otherUserCreatePayloadADM.password.get.value should not equal request.password + otherUserCreatePayloadADM.givenName.get.value should not equal request.givenName + otherUserCreatePayloadADM.familyName.get.value should not equal request.familyName + otherUserCreatePayloadADM.status.get.value should not equal request.status + otherUserCreatePayloadADM.lang.get.value should not equal request.lang + otherUserCreatePayloadADM.systemAdmin.get.value should not equal request.systemAdmin + } + + "throw 'BadRequestException' if 'username' is missing" in { + val request = createUserApiRequestADM(username = "") + + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Missing username" + } + + "throw 'BadRequestException' if 'email' is missing" in { + val request = createUserApiRequestADM(email = "") + + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Missing email" + } + + "throw 'BadRequestException' if 'password' is missing" in { + val request = createUserApiRequestADM(password = "") + + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Missing password" + } + + "throw 'BadRequestException' if 'givenName' is missing" in { + val request = createUserApiRequestADM(givenName = "") + + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Missing given name" + } + + "throw 'BadRequestException' if 'familyName' is missing" in { + val request = createUserApiRequestADM(familyName = "") + + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Missing family name" + } + + "throw 'BadRequestException' if 'lang' is missing" in { + val request = createUserApiRequestADM(lang = "") + + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Missing language code" + } + + "throw 'BadRequestException' if the supplied 'id' is not a valid IRI" in { + val request = createUserApiRequestADM(id = Some("invalid-iri")) + + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Invalid user IRI" + + } + + "throw 'BadRequestException' if 'username' is invalid" in { + Set( + createUserApiRequestADM(username = "don"), // too short + createUserApiRequestADM(username = "asdfoiasdfasdnlasdkjflasdjfaskdjflaskdjfaddssdskdfjs"), // too long + createUserApiRequestADM(username = "_donald"), // starts with _ + createUserApiRequestADM(username = ".donald"), // starts with . + createUserApiRequestADM(username = "donald_"), // ends with _ + createUserApiRequestADM(username = "donald."), // ends with . + createUserApiRequestADM(username = "donald__duck"), // contains multiple _ + createUserApiRequestADM(username = "donald..duck"), // contains multiple . + createUserApiRequestADM(username = "donald#duck"), // contains not only alphanumeric characters + createUserApiRequestADM(username = "dönälddück") // contains umlaut characters + ).map(request => + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Invalid username" + ) + } + + "throw 'BadRequestException' if 'email' is invalid" in { + Set( + createUserApiRequestADM(email = "don"), // does not contain @ + createUserApiRequestADM(email = "don@"), // ends with @ + createUserApiRequestADM(email = "@don") // starts with @ + ).map(request => + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Invalid email" + ) + } + + "throw 'BadRequestException' if 'lang' is invalid" in { + val request = createUserApiRequestADM(lang = "xy") + + the[BadRequestException] thrownBy createUserCreatePayloadADM(request) should have message "Invalid language code" + } + + } + +} diff --git a/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala b/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala index b4ba6c3401..0bed67bcd7 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/admin/UsersResponderADMSpec.scala @@ -20,7 +20,6 @@ package org.knora.webapi.responders.admin import java.util.UUID - import akka.actor.Status.Failure import akka.testkit.ImplicitSender import com.typesafe.config.{Config, ConfigFactory} @@ -29,7 +28,7 @@ import org.knora.webapi.exceptions.{BadRequestException, DuplicateValueException import org.knora.webapi.messages.StringFormatter import org.knora.webapi.messages.admin.responder.groupsmessages.{GroupMembersGetRequestADM, GroupMembersGetResponseADM} import org.knora.webapi.messages.admin.responder.projectsmessages._ -import org.knora.webapi.messages.admin.responder.usersmessages._ +import org.knora.webapi.messages.admin.responder.usersmessages.{UserChangeRequestADM, _} import org.knora.webapi.messages.util.KnoraSystemInstances import org.knora.webapi.messages.v2.routing.authenticationmessages.KnoraPasswordCredentialsV2 import org.knora.webapi.routing.Authenticator @@ -47,19 +46,19 @@ object UsersResponderADMSpec { } /** - * This spec is used to test the messages received by the [[UsersResponderADM]] actor. - */ + * This spec is used to test the messages received by the [[UsersResponderADM]] actor. + */ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with ImplicitSender with Authenticator { private val timeout: FiniteDuration = 8.seconds - private val rootUser = SharedTestDataADM.rootUser + private val rootUser = SharedTestDataADM.rootUser private val anythingAdminUser = SharedTestDataADM.anythingAdminUser - private val normalUser = SharedTestDataADM.normalUser + private val normalUser = SharedTestDataADM.normalUser private val incunabulaUser = SharedTestDataADM.incunabulaProjectAdminUser - private val imagesProject = SharedTestDataADM.imagesProject + private val imagesProject = SharedTestDataADM.imagesProject private val imagesReviewerGroup = SharedTestDataADM.imagesReviewerGroup implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance @@ -68,30 +67,38 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "asked about all users" should { "return a list if asked by SystemAdmin" in { - responderManager ! UsersGetRequestADM(featureFactoryConfig = defaultFeatureFactoryConfig, - requestingUser = rootUser) + responderManager ! UsersGetRequestADM( + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = rootUser + ) val response = expectMsgType[UsersGetResponseADM](timeout) response.users.nonEmpty should be(true) response.users.size should be(18) } "return a list if asked by ProjectAdmin" in { - responderManager ! UsersGetRequestADM(featureFactoryConfig = defaultFeatureFactoryConfig, - requestingUser = anythingAdminUser) + responderManager ! UsersGetRequestADM( + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingAdminUser + ) val response = expectMsgType[UsersGetResponseADM](timeout) response.users.nonEmpty should be(true) response.users.size should be(18) } "return 'ForbiddenException' if asked by normal user'" in { - responderManager ! UsersGetRequestADM(featureFactoryConfig = defaultFeatureFactoryConfig, - requestingUser = normalUser) + responderManager ! UsersGetRequestADM( + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = normalUser + ) expectMsg(timeout, Failure(ForbiddenException("ProjectAdmin or SystemAdmin permissions are required."))) } "not return the system and anonymous users" in { - responderManager ! UsersGetRequestADM(featureFactoryConfig = defaultFeatureFactoryConfig, - requestingUser = rootUser) + responderManager ! UsersGetRequestADM( + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = rootUser + ) val response = expectMsgType[UsersGetResponseADM](timeout) response.users.nonEmpty should be(true) response.users.size should be(18) @@ -233,15 +240,17 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "CREATE the user and return it's profile if the supplied email is unique " in { responderManager ! UserCreateRequestADM( - createRequest = CreateUserApiRequestADM( - username = "donald.duck", - email = "donald.duck@example.com", - givenName = "Donald", - familyName = "Duck", - password = "test", - status = true, - lang = "en", - systemAdmin = false + userCreatePayloadADM = UserCreatePayloadADM.create( + username = Username.create("donald.duck").fold(error => throw error, value => value), + email = Email + .create("donald.duck@example.com") + .fold(error => throw error, value => value), + givenName = GivenName.create("Donald").fold(error => throw error, value => value), + familyName = FamilyName.create("Duck").fold(error => throw error, value => value), + password = Password.create("test").fold(error => throw error, value => value), + status = Status.create(true).fold(error => throw error, value => value), + lang = LanguageCode.create("en").fold(error => throw error, value => value), + systemAdmin = SystemAdmin.create(false).fold(error => throw error, value => value) ), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.anonymousUser, @@ -258,15 +267,17 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "return a 'DuplicateValueException' if the supplied 'username' is not unique" in { responderManager ! UserCreateRequestADM( - createRequest = CreateUserApiRequestADM( - username = "root", - email = "root2@example.com", - givenName = "Donal", - familyName = "Duck", - password = "test", - status = true, - lang = "en", - systemAdmin = false + userCreatePayloadADM = UserCreatePayloadADM.create( + username = Username.create("root").fold(error => throw error, value => value), + email = Email + .create("root2@example.com") + .fold(error => throw error, value => value), + givenName = GivenName.create("Donald").fold(error => throw error, value => value), + familyName = FamilyName.create("Duck").fold(error => throw error, value => value), + password = Password.create("test").fold(error => throw error, value => value), + status = Status.create(true).fold(error => throw error, value => value), + lang = LanguageCode.create("en").fold(error => throw error, value => value), + systemAdmin = SystemAdmin.create(false).fold(error => throw error, value => value) ), featureFactoryConfig = defaultFeatureFactoryConfig, SharedTestDataADM.anonymousUser, @@ -277,15 +288,17 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "return a 'DuplicateValueException' if the supplied 'email' is not unique" in { responderManager ! UserCreateRequestADM( - createRequest = CreateUserApiRequestADM( - username = "root2", - email = "root@example.com", - givenName = "Donal", - familyName = "Duck", - password = "test", - status = true, - lang = "en", - systemAdmin = false + userCreatePayloadADM = UserCreatePayloadADM.create( + username = Username.create("root2").fold(error => throw error, value => value), + email = Email + .create("root@example.com") + .fold(error => throw error, value => value), + givenName = GivenName.create("Donald").fold(error => throw error, value => value), + familyName = FamilyName.create("Duck").fold(error => throw error, value => value), + password = Password.create("test").fold(error => throw error, value => value), + status = Status.create(true).fold(error => throw error, value => value), + lang = LanguageCode.create("en").fold(error => throw error, value => value), + systemAdmin = SystemAdmin.create(false).fold(error => throw error, value => value) ), featureFactoryConfig = defaultFeatureFactoryConfig, SharedTestDataADM.anonymousUser, @@ -293,64 +306,6 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with ) expectMsg(Failure(DuplicateValueException(s"User with the email 'root@example.com' already exists"))) } - - "return a 'BadRequestException' if the supplied 'username' contains invalid characters (@)" in { - responderManager ! UserCreateRequestADM( - createRequest = CreateUserApiRequestADM( - username = "donald.duck2@example.com", - email = "donald.duck2@example.com", - givenName = "Donal", - familyName = "Duck", - password = "test", - status = true, - lang = "en", - systemAdmin = false - ), - featureFactoryConfig = defaultFeatureFactoryConfig, - SharedTestDataADM.anonymousUser, - UUID.randomUUID - ) - expectMsg(Failure(BadRequestException(s"The username 'donald.duck2@example.com' contains invalid characters"))) - } - - "return a 'BadRequestException' if the supplied 'username' contains invalid characters (-)" in { - responderManager ! UserCreateRequestADM( - createRequest = CreateUserApiRequestADM( - username = "donald-duck", - email = "donald.duck2@example.com", - givenName = "Donal", - familyName = "Duck", - password = "test", - status = true, - lang = "en", - systemAdmin = false - ), - featureFactoryConfig = defaultFeatureFactoryConfig, - SharedTestDataADM.anonymousUser, - UUID.randomUUID - ) - expectMsg(Failure(BadRequestException(s"The username 'donald-duck' contains invalid characters"))) - } - - "return a 'BadRequestException' if the supplied 'email' is invalid" in { - responderManager ! UserCreateRequestADM( - createRequest = CreateUserApiRequestADM( - username = "root3", - email = "root3", - givenName = "Donal", - familyName = "Duck", - password = "test", - status = true, - lang = "en", - systemAdmin = false - ), - featureFactoryConfig = defaultFeatureFactoryConfig, - SharedTestDataADM.anonymousUser, - UUID.randomUUID - ) - expectMsg(Failure(BadRequestException(s"The email 'root3' is invalid"))) - } - } "asked to update a user" should { @@ -358,137 +313,101 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "UPDATE the user's basic information" in { /* User information is updated by the user */ - responderManager ! UserChangeBasicUserInformationRequestADM( + responderManager ! UserChangeBasicInformationRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - email = None, - givenName = Some("Donald"), - familyName = None, - lang = None + userUpdateBasicInformationPayload = UserUpdateBasicInformationPayloadADM( + givenName = Some(GivenName.create("Donald").fold(error => throw error, value => value)) ), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.normalUser, - UUID.randomUUID + apiRequestID = UUID.randomUUID ) val response1 = expectMsgType[UserOperationResponseADM](timeout) response1.user.givenName should equal("Donald") /* User information is updated by a system admin */ - responderManager ! UserChangeBasicUserInformationRequestADM( + responderManager ! UserChangeBasicInformationRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - email = None, - givenName = None, - familyName = Some("Duck"), - lang = None + userUpdateBasicInformationPayload = UserUpdateBasicInformationPayloadADM( + familyName = Some(FamilyName.create("Duck").fold(error => throw error, value => value)) ), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.superUser, - UUID.randomUUID + apiRequestID = UUID.randomUUID ) val response2 = expectMsgType[UserOperationResponseADM](timeout) response2.user.familyName should equal("Duck") /* User information is updated by a system admin */ - responderManager ! UserChangeBasicUserInformationRequestADM( + responderManager ! UserChangeBasicInformationRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - email = None, - givenName = Some(SharedTestDataADM.normalUser.givenName), - familyName = Some(SharedTestDataADM.normalUser.familyName), - lang = None + userUpdateBasicInformationPayload = UserUpdateBasicInformationPayloadADM( + givenName = + Some(GivenName.create(SharedTestDataADM.normalUser.givenName).fold(error => throw error, value => value)), + familyName = Some( + FamilyName.create(SharedTestDataADM.normalUser.familyName).fold(error => throw error, value => value) + ) ), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.superUser, - UUID.randomUUID + apiRequestID = UUID.randomUUID ) val response3 = expectMsgType[UserOperationResponseADM](timeout) response3.user.givenName should equal(SharedTestDataADM.normalUser.givenName) response3.user.familyName should equal(SharedTestDataADM.normalUser.familyName) - } "return a 'DuplicateValueException' if the supplied 'username' is not unique" in { - responderManager ! UserChangeBasicUserInformationRequestADM( + val duplicateUsername = + Some(Username.create(SharedTestDataADM.anythingUser1.username).fold(error => throw error, value => value)) + responderManager ! UserChangeBasicInformationRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - username = Some("root") + userUpdateBasicInformationPayload = UserUpdateBasicInformationPayloadADM( + username = duplicateUsername ), featureFactoryConfig = defaultFeatureFactoryConfig, SharedTestDataADM.superUser, UUID.randomUUID ) - expectMsg(Failure(DuplicateValueException(s"User with the username 'root' already exists"))) + expectMsg( + Failure( + DuplicateValueException( + s"User with the username '${SharedTestDataADM.anythingUser1.username}' already exists" + ) + ) + ) } "return a 'DuplicateValueException' if the supplied 'email' is not unique" in { - responderManager ! UserChangeBasicUserInformationRequestADM( + val duplicateEmail = + Some(Email.create(SharedTestDataADM.anythingUser1.email).fold(error => throw error, value => value)) + responderManager ! UserChangeBasicInformationRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - email = Some("root@example.com") + userUpdateBasicInformationPayload = UserUpdateBasicInformationPayloadADM( + email = duplicateEmail ), featureFactoryConfig = defaultFeatureFactoryConfig, SharedTestDataADM.superUser, UUID.randomUUID ) - expectMsg(Failure(DuplicateValueException(s"User with the email 'root@example.com' already exists"))) - } - - "return 'BadRequest' if the new 'username' contains invalid characters (@)" in { - - responderManager ! UserChangeBasicUserInformationRequestADM( - userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - username = Some("donald.duck2@example.com") - ), - featureFactoryConfig = defaultFeatureFactoryConfig, - requestingUser = SharedTestDataADM.superUser, - UUID.randomUUID() - ) - - expectMsg(timeout, - Failure(BadRequestException(s"The username 'donald.duck2@example.com' contains invalid characters"))) - } - - "return 'BadRequest' if the new 'username' contains invalid characters (-)" in { - - responderManager ! UserChangeBasicUserInformationRequestADM( - userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - username = Some("donald-duck") - ), - featureFactoryConfig = defaultFeatureFactoryConfig, - requestingUser = SharedTestDataADM.superUser, - UUID.randomUUID() - ) - - expectMsg(timeout, Failure(BadRequestException(s"The username 'donald-duck' contains invalid characters"))) - } - - "return 'BadRequest' if the new 'email' is invalid" in { - - responderManager ! UserChangeBasicUserInformationRequestADM( - userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - email = Some("root3") - ), - featureFactoryConfig = defaultFeatureFactoryConfig, - requestingUser = SharedTestDataADM.superUser, - UUID.randomUUID() + expectMsg( + Failure( + DuplicateValueException(s"User with the email '${SharedTestDataADM.anythingUser1.email}' already exists") + ) ) - - expectMsg(timeout, Failure(BadRequestException(s"The email address 'root3' is invalid"))) } "UPDATE the user's password (by himself)" in { + val requesterPassword = Password.create("test").fold(error => throw error, value => value) + val newPassword = Password.create("test123456").fold(error => throw error, value => value) responderManager ! UserChangePasswordRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - requesterPassword = Some("test"), // of the requesting user - newPassword = Some("test123456") + userUpdatePasswordPayload = UserUpdatePasswordPayloadADM( + requesterPassword = requesterPassword, + newPassword = newPassword ), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.normalUser, @@ -501,7 +420,7 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with val resF = Authenticator.authenticateCredentialsV2( credentials = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test123456")), - featureFactoryConfig = defaultFeatureFactoryConfig, + featureFactoryConfig = defaultFeatureFactoryConfig )(system, responderManager, executionContext) resF map { res => @@ -510,11 +429,14 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with } "UPDATE the user's password (by a system admin)" in { + val requesterPassword = Password.create("test").fold(error => throw error, value => value) + val newPassword = Password.create("test654321").fold(error => throw error, value => value) + responderManager ! UserChangePasswordRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM( - requesterPassword = Some("test"), // of the requesting user - newPassword = Some("test654321") + userUpdatePasswordPayload = UserUpdatePasswordPayloadADM( + requesterPassword = requesterPassword, + newPassword = newPassword ), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.rootUser, @@ -527,7 +449,7 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with val resF = Authenticator.authenticateCredentialsV2( credentials = Some(KnoraPasswordCredentialsV2(UserIdentifierADM(maybeEmail = Some(normalUser.email)), "test654321")), - featureFactoryConfig = defaultFeatureFactoryConfig, + featureFactoryConfig = defaultFeatureFactoryConfig )(system, responderManager, executionContext) resF map { res => @@ -538,7 +460,7 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "UPDATE the user's status, (deleting) making him inactive " in { responderManager ! UserChangeStatusRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM(status = Some(false)), + status = Status.create(false).fold(error => throw error, value => value), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.superUser, UUID.randomUUID() @@ -549,7 +471,7 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with responderManager ! UserChangeStatusRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM(status = Some(true)), + status = Status.create(true).fold(error => throw error, value => value), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.superUser, UUID.randomUUID() @@ -562,7 +484,7 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "UPDATE the user's system admin membership" in { responderManager ! UserChangeSystemAdminMembershipStatusRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM(systemAdmin = Some(true)), + systemAdmin = SystemAdmin.create(true).fold(error => throw error, value => value), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.superUser, UUID.randomUUID() @@ -573,7 +495,7 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with responderManager ! UserChangeSystemAdminMembershipStatusRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM(systemAdmin = Some(false)), + systemAdmin = SystemAdmin.create(false).fold(error => throw error, value => value), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.superUser, UUID.randomUUID() @@ -586,11 +508,11 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "return a 'ForbiddenException' if the user requesting update is not the user itself or system admin" in { /* User information is updated by other normal user */ - responderManager ! UserChangeBasicUserInformationRequestADM( + responderManager ! UserChangeBasicInformationRequestADM( userIri = SharedTestDataADM.superUser.id, - changeUserRequest = ChangeUserApiRequestADM( + userUpdateBasicInformationPayload = UserUpdateBasicInformationPayloadADM( email = None, - givenName = Some("Donald"), + givenName = Some(GivenName.create("Donald").fold(error => throw error, value => value)), familyName = None, lang = None ), @@ -601,14 +523,16 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with expectMsg( timeout, Failure( - ForbiddenException("User information can only be changed by the user itself or a system administrator"))) + ForbiddenException("User information can only be changed by the user itself or a system administrator") + ) + ) /* Password is updated by other normal user */ responderManager ! UserChangePasswordRequestADM( userIri = SharedTestDataADM.superUser.id, - changeUserRequest = ChangeUserApiRequestADM( - requesterPassword = Some("test"), - newPassword = Some("test123456") + userUpdatePasswordPayload = UserUpdatePasswordPayloadADM( + requesterPassword = Password.create("test").fold(error => throw error, value => value), + newPassword = Password.create("test123456").fold(error => throw error, value => value) ), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.normalUser, @@ -616,38 +540,43 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with ) expectMsg( timeout, - Failure(ForbiddenException("User's password can only be changed by the user itself or a system admin."))) + Failure( + ForbiddenException("User's password can only be changed by the user itself or a system administrator") + ) + ) /* Status is updated by other normal user */ responderManager ! UserChangeStatusRequestADM( userIri = SharedTestDataADM.superUser.id, - changeUserRequest = ChangeUserApiRequestADM(status = Some(false)), + status = Status.create(false).fold(error => throw error, value => value), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.normalUser, UUID.randomUUID ) expectMsg( timeout, - Failure(ForbiddenException("User's status can only be changed by the user itself or a system administrator"))) + Failure(ForbiddenException("User's status can only be changed by the user itself or a system administrator")) + ) /* System admin group membership */ responderManager ! UserChangeSystemAdminMembershipStatusRequestADM( userIri = SharedTestDataADM.normalUser.id, - changeUserRequest = ChangeUserApiRequestADM(systemAdmin = Some(true)), + systemAdmin = SystemAdmin.create(true).fold(error => throw error, value => value), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.normalUser, UUID.randomUUID() ) expectMsg( timeout, - Failure(ForbiddenException("User's system admin membership can only be changed by a system administrator"))) + Failure(ForbiddenException("User's system admin membership can only be changed by a system administrator")) + ) } "return 'BadRequest' if system user is requested to change" in { responderManager ! UserChangeStatusRequestADM( userIri = KnoraSystemInstances.Users.SystemUser.id, - changeUserRequest = ChangeUserApiRequestADM(status = Some(false)), + status = Status.create(false).fold(error => throw error, value => value), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.superUser, UUID.randomUUID() @@ -660,7 +589,7 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with responderManager ! UserChangeStatusRequestADM( userIri = KnoraSystemInstances.Users.AnonymousUser.id, - changeUserRequest = ChangeUserApiRequestADM(status = Some(false)), + status = Status.create(false).fold(error => throw error, value => value), featureFactoryConfig = defaultFeatureFactoryConfig, requestingUser = SharedTestDataADM.superUser, UUID.randomUUID() @@ -668,18 +597,6 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with expectMsg(timeout, Failure(BadRequestException("Changes to built-in users are not allowed."))) } - - "return 'BadRequest' if nothing would be changed during the update" in { - - an[BadRequestException] should be thrownBy ChangeUserApiRequestADM(None, - None, - None, - None, - None, - None, - None, - None) - } } "asked to update the user's project membership" should { @@ -690,11 +607,13 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with val membershipsBeforeUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) membershipsBeforeUpdate.projects should equal(Seq()) - responderManager ! UserProjectMembershipAddRequestADM(normalUser.id, - imagesProject.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserProjectMembershipAddRequestADM( + normalUser.id, + imagesProject.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) val membershipUpdateResponse = expectMsgType[UserOperationResponseADM](timeout) responderManager ! UserProjectMembershipsGetRequestADM(normalUser.id, defaultFeatureFactoryConfig, rootUser) @@ -717,11 +636,13 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with val membershipsBeforeUpdate = expectMsgType[UserProjectMembershipsGetResponseADM](timeout) membershipsBeforeUpdate.projects should equal(Seq(imagesProject)) - responderManager ! UserProjectMembershipRemoveRequestADM(normalUser.id, - imagesProject.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserProjectMembershipRemoveRequestADM( + normalUser.id, + imagesProject.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) expectMsgType[UserOperationResponseADM](timeout) responderManager ! UserProjectMembershipsGetRequestADM(normalUser.id, defaultFeatureFactoryConfig, rootUser) @@ -741,26 +662,34 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "return a 'ForbiddenException' if the user requesting update is not the project or system admin" in { /* User is added to a project by a normal user */ - responderManager ! UserProjectMembershipAddRequestADM(normalUser.id, - imagesProject.id, - defaultFeatureFactoryConfig, - normalUser, - UUID.randomUUID()) + responderManager ! UserProjectMembershipAddRequestADM( + normalUser.id, + imagesProject.id, + defaultFeatureFactoryConfig, + normalUser, + UUID.randomUUID() + ) expectMsg( timeout, Failure( - ForbiddenException("User's project membership can only be changed by a project or system administrator"))) + ForbiddenException("User's project membership can only be changed by a project or system administrator") + ) + ) /* User is removed from a project by a normal user */ - responderManager ! UserProjectMembershipRemoveRequestADM(normalUser.id, - imagesProject.id, - defaultFeatureFactoryConfig, - normalUser, - UUID.randomUUID()) + responderManager ! UserProjectMembershipRemoveRequestADM( + normalUser.id, + imagesProject.id, + defaultFeatureFactoryConfig, + normalUser, + UUID.randomUUID() + ) expectMsg( timeout, Failure( - ForbiddenException("User's project membership can only be changed by a project or system administrator"))) + ForbiddenException("User's project membership can only be changed by a project or system administrator") + ) + ) } } @@ -769,24 +698,30 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "ADD user to project admin group" in { - responderManager ! UserProjectAdminMembershipsGetRequestADM(normalUser.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserProjectAdminMembershipsGetRequestADM( + normalUser.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) val membershipsBeforeUpdate = expectMsgType[UserProjectAdminMembershipsGetResponseADM](timeout) membershipsBeforeUpdate.projects should equal(Seq()) - responderManager ! UserProjectAdminMembershipAddRequestADM(normalUser.id, - imagesProject.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserProjectAdminMembershipAddRequestADM( + normalUser.id, + imagesProject.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) expectMsgType[UserOperationResponseADM](timeout) - responderManager ! UserProjectAdminMembershipsGetRequestADM(normalUser.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserProjectAdminMembershipsGetRequestADM( + normalUser.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) val membershipsAfterUpdate = expectMsgType[UserProjectAdminMembershipsGetResponseADM](timeout) membershipsAfterUpdate.projects should equal(Seq(imagesProject)) @@ -801,24 +736,30 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with } "DELETE user from project admin group" in { - responderManager ! UserProjectAdminMembershipsGetRequestADM(normalUser.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserProjectAdminMembershipsGetRequestADM( + normalUser.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) val membershipsBeforeUpdate = expectMsgType[UserProjectAdminMembershipsGetResponseADM](timeout) membershipsBeforeUpdate.projects should equal(Seq(imagesProject)) - responderManager ! UserProjectAdminMembershipRemoveRequestADM(normalUser.id, - imagesProject.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserProjectAdminMembershipRemoveRequestADM( + normalUser.id, + imagesProject.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) expectMsgType[UserOperationResponseADM](timeout) - responderManager ! UserProjectAdminMembershipsGetRequestADM(normalUser.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserProjectAdminMembershipsGetRequestADM( + normalUser.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) val membershipsAfterUpdate = expectMsgType[UserProjectAdminMembershipsGetResponseADM](timeout) membershipsAfterUpdate.projects should equal(Seq()) @@ -835,26 +776,38 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "return a 'ForbiddenException' if the user requesting update is not the project or system admin" in { /* User is added to a project by a normal user */ - responderManager ! UserProjectAdminMembershipAddRequestADM(normalUser.id, - imagesProject.id, - defaultFeatureFactoryConfig, - normalUser, - UUID.randomUUID()) - expectMsg(timeout, - Failure( - ForbiddenException( - "User's project admin membership can only be changed by a project or system administrator"))) + responderManager ! UserProjectAdminMembershipAddRequestADM( + normalUser.id, + imagesProject.id, + defaultFeatureFactoryConfig, + normalUser, + UUID.randomUUID() + ) + expectMsg( + timeout, + Failure( + ForbiddenException( + "User's project admin membership can only be changed by a project or system administrator" + ) + ) + ) /* User is removed from a project by a normal user */ - responderManager ! UserProjectAdminMembershipRemoveRequestADM(normalUser.id, - imagesProject.id, - defaultFeatureFactoryConfig, - normalUser, - UUID.randomUUID()) - expectMsg(timeout, - Failure( - ForbiddenException( - "User's project admin membership can only be changed by a project or system administrator"))) + responderManager ! UserProjectAdminMembershipRemoveRequestADM( + normalUser.id, + imagesProject.id, + defaultFeatureFactoryConfig, + normalUser, + UUID.randomUUID() + ) + expectMsg( + timeout, + Failure( + ForbiddenException( + "User's project admin membership can only be changed by a project or system administrator" + ) + ) + ) } } @@ -866,11 +819,13 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with val membershipsBeforeUpdate = expectMsgType[UserGroupMembershipsGetResponseADM](timeout) membershipsBeforeUpdate.groups should equal(Seq()) - responderManager ! UserGroupMembershipAddRequestADM(normalUser.id, - imagesReviewerGroup.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserGroupMembershipAddRequestADM( + normalUser.id, + imagesReviewerGroup.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) expectMsgType[UserOperationResponseADM](timeout) responderManager ! UserGroupMembershipsGetRequestADM(normalUser.id, defaultFeatureFactoryConfig, rootUser) @@ -892,11 +847,13 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with val membershipsBeforeUpdate = expectMsgType[UserGroupMembershipsGetResponseADM](timeout) membershipsBeforeUpdate.groups.map(_.id) should equal(Seq(imagesReviewerGroup.id)) - responderManager ! UserGroupMembershipRemoveRequestADM(normalUser.id, - imagesReviewerGroup.id, - defaultFeatureFactoryConfig, - rootUser, - UUID.randomUUID()) + responderManager ! UserGroupMembershipRemoveRequestADM( + normalUser.id, + imagesReviewerGroup.id, + defaultFeatureFactoryConfig, + rootUser, + UUID.randomUUID() + ) expectMsgType[UserOperationResponseADM](timeout) responderManager ! UserGroupMembershipsGetRequestADM(normalUser.id, defaultFeatureFactoryConfig, rootUser) @@ -916,26 +873,34 @@ class UsersResponderADMSpec extends CoreSpec(UsersResponderADMSpec.config) with "return a 'ForbiddenException' if the user requesting update is not the project or system admin" in { /* User is added to a project by a normal user */ - responderManager ! UserGroupMembershipAddRequestADM(normalUser.id, - imagesReviewerGroup.id, - defaultFeatureFactoryConfig, - normalUser, - UUID.randomUUID()) + responderManager ! UserGroupMembershipAddRequestADM( + normalUser.id, + imagesReviewerGroup.id, + defaultFeatureFactoryConfig, + normalUser, + UUID.randomUUID() + ) expectMsg( timeout, Failure( - ForbiddenException("User's group membership can only be changed by a project or system administrator"))) + ForbiddenException("User's group membership can only be changed by a project or system administrator") + ) + ) /* User is removed from a project by a normal user */ - responderManager ! UserGroupMembershipRemoveRequestADM(normalUser.id, - imagesReviewerGroup.id, - defaultFeatureFactoryConfig, - normalUser, - UUID.randomUUID()) + responderManager ! UserGroupMembershipRemoveRequestADM( + normalUser.id, + imagesReviewerGroup.id, + defaultFeatureFactoryConfig, + normalUser, + UUID.randomUUID() + ) expectMsg( timeout, Failure( - ForbiddenException("User's group membership can only be changed by a project or system administrator"))) + ForbiddenException("User's group membership can only be changed by a project or system administrator") + ) + ) } }