From 2fed15e0c75e8274f6c40e1d3c85528e947c8bd3 Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 10 Jul 2023 14:32:39 +0200 Subject: [PATCH 01/11] Add Super admin routes for user and orga management --- .../AuthenticationController.scala | 4 +- app/controllers/OrganizationController.scala | 62 +++++++++++++++---- app/controllers/UserController.scala | 35 ++++++++++- .../organization/OrganizationService.scala | 1 - app/models/user/UserService.scala | 40 ++++++++++++ conf/messages | 1 + conf/webknossos.latest.routes | 3 + 7 files changed, 128 insertions(+), 18 deletions(-) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index 696d771e70e..23b569652bd 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -234,8 +234,8 @@ class AuthenticationController @Inject()( workflowHash: Option[String]): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { - isSuperuser <- multiUserDAO.findOne(request.identity._multiUser).map(_.isSuperUser) - selectedOrganization <- if (isSuperuser) + isSuperUser <- multiUserDAO.findOne(request.identity._multiUser).map(_.isSuperUser) + selectedOrganization <- if (isSuperUser) accessibleBySwitchingForSuperUser(organizationName, dataSetName, annotationId, workflowHash) else accessibleBySwitchingForMultiUser(request.identity._multiUser, diff --git a/app/controllers/OrganizationController.scala b/app/controllers/OrganizationController.scala index a00e4833f37..2492cbef9bd 100755 --- a/app/controllers/OrganizationController.scala +++ b/app/controllers/OrganizationController.scala @@ -13,8 +13,8 @@ import models.team.PricingPlan import oxalis.security.{WkEnv, WkSilhouetteEnvironment} import play.api.i18n.Messages import play.api.libs.functional.syntax._ -import play.api.libs.json.{JsNull, JsValue, Json, __} -import play.api.mvc.{Action, AnyContent} +import play.api.libs.json.{JsNull, JsValue, Json, OFormat, __} +import play.api.mvc.{Action, AnyContent, PlayBodyParsers} import utils.WkConf import scala.concurrent.duration._ @@ -22,17 +22,18 @@ import oxalis.mail.{DefaultMails, Send} import scala.concurrent.ExecutionContext -class OrganizationController @Inject()(organizationDAO: OrganizationDAO, - organizationService: OrganizationService, - inviteDAO: InviteDAO, - conf: WkConf, - userDAO: UserDAO, - multiUserDAO: MultiUserDAO, - wkSilhouetteEnvironment: WkSilhouetteEnvironment, - userService: UserService, - defaultMails: DefaultMails, - actorSystem: ActorSystem, - sil: Silhouette[WkEnv])(implicit ec: ExecutionContext) +class OrganizationController @Inject()( + organizationDAO: OrganizationDAO, + organizationService: OrganizationService, + inviteDAO: InviteDAO, + conf: WkConf, + userDAO: UserDAO, + multiUserDAO: MultiUserDAO, + wkSilhouetteEnvironment: WkSilhouetteEnvironment, + userService: UserService, + defaultMails: DefaultMails, + actorSystem: ActorSystem, + sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, val bodyParsers: PlayBodyParsers) extends Controller with FoxImplicits { @@ -64,6 +65,24 @@ class OrganizationController @Inject()(organizationDAO: OrganizationDAO, } yield Ok(Json.toJson(js)) } + case class OrganizationCreationParameters(orgName: Option[String], + orgDisplayName: String, + ownerEmail: Option[String], + ownerId: Option[String]) + object OrganizationCreationParameters { + implicit val jsonFormat: OFormat[OrganizationCreationParameters] = Json.format[OrganizationCreationParameters] + } + def create: Action[OrganizationCreationParameters] = + sil.SecuredAction.async(validateJson[OrganizationCreationParameters]) { implicit request => + for { + isSuperUser <- multiUserDAO.findOne(request.identity._multiUser).map(_.isSuperUser) + _ <- bool2Fox(isSuperUser) ?~> "notAllowed" ~> FORBIDDEN + ownerId <- userService.getMultiUserId(request.body.ownerId, request.body.ownerEmail) + org <- organizationService.createOrganization(request.body.orgName, request.body.orgDisplayName) + _ <- userService.createUserInOrganization(ownerId, org._id, asOwner = true) + } yield Ok(org.name) + } + def getDefault: Action[AnyContent] = Action.async { implicit request => for { allOrgs <- organizationDAO.findAll(GlobalAccessContext) ?~> "organization.list.failed" @@ -154,6 +173,23 @@ class OrganizationController @Inject()(organizationDAO: OrganizationDAO, } yield Ok } + case class AddUserParameters(userEmail: Option[String], multiUserId: Option[String]) + object AddUserParameters { + implicit val jsonFormat: OFormat[AddUserParameters] = Json.format[AddUserParameters] + } + + def addUser(organizationName: String): Action[AddUserParameters] = + sil.SecuredAction.async(validateJson[AddUserParameters]) { implicit request => + for { + isSuperUser <- multiUserDAO.findOne(request.identity._multiUser).map(_.isSuperUser) + _ <- bool2Fox(isSuperUser) ?~> "notAllowed" ~> FORBIDDEN + multiUserId <- userService.getMultiUserId(request.body.multiUserId, request.body.userEmail) + organization <- organizationDAO.findOneByName(organizationName) ?~> Messages("organization.notFound", + organizationName) ~> NOT_FOUND + user <- userService.createUserInOrganization(multiUserId, organization._id, asOwner = false) + } yield Ok(user._id.toString) + } + private val organizationUpdateReads = ((__ \ 'displayName).read[String] and (__ \ 'newUserMailingList).read[String]).tupled diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 76354ed1d3f..c0d82638e70 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -6,11 +6,11 @@ import com.scalableminds.util.mvc.Filter import com.scalableminds.util.tools.{Fox, FoxImplicits} import io.swagger.annotations._ import models.annotation.{AnnotationDAO, AnnotationService, AnnotationType} -import models.organization.OrganizationService +import models.organization.{OrganizationDAO, OrganizationService} import models.team._ import models.user._ import models.user.time._ -import oxalis.security.WkEnv +import oxalis.security.{PasswordHasher, WkEnv} import play.api.i18n.{Messages, MessagesProvider} import play.api.libs.functional.syntax._ import play.api.libs.json.Json._ @@ -29,10 +29,12 @@ class UserController @Inject()(userService: UserService, multiUserDAO: MultiUserDAO, organizationService: OrganizationService, annotationDAO: AnnotationDAO, + organizationDAO: OrganizationDAO, timeSpanService: TimeSpanService, teamMembershipService: TeamMembershipService, annotationService: AnnotationService, teamDAO: TeamDAO, + passwordHasher: PasswordHasher, sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers) extends Controller with FoxImplicits { @@ -461,4 +463,33 @@ class UserController @Inject()(userService: UserService, } yield Ok(updatedJs) } + case class CreateUserInOrganizationParameters(firstName: String, + lastName: String, + email: String, + password: Option[String]) + + object CreateUserInOrganizationParameters { + implicit val jsonFormat: OFormat[CreateUserInOrganizationParameters] = + Json.format[CreateUserInOrganizationParameters] + } + def createInOrganization(organizationName: String): Action[CreateUserInOrganizationParameters] = + sil.SecuredAction.async(validateJson[CreateUserInOrganizationParameters]) { implicit request => + for { + isSuperUser <- multiUserDAO.findOne(request.identity._multiUser).map(_.isSuperUser) + _ <- bool2Fox(isSuperUser) ?~> "notAllowed" ~> FORBIDDEN + organization <- organizationDAO.findOneByName(organizationName) ?~> "organization.notFound" + passwordInfo = request.body.password + .map(passwordHasher.hash) + .getOrElse(userService.getOpenIdConnectPasswordInfo) + user <- userService.insert(organization._id, + request.body.email, + request.body.firstName, + request.body.lastName, + isActive = true, + passwordInfo, + isAdmin = false, + isOrganizationOwner = false) + } yield Ok(user._id.toString) + } + } diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala index 2fd83784ab7..eaa37a34c5c 100644 --- a/app/models/organization/OrganizationService.scala +++ b/app/models/organization/OrganizationService.scala @@ -137,5 +137,4 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, _ <- Fox.runOptional(organization.includedUsers)(includedUsers => bool2Fox(userCount + usersToAddCount <= includedUsers)) } yield () - } diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index 14b234313d0..513a12a4991 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -317,6 +317,46 @@ class UserService @Inject()(conf: WkConf, teamMemberships <- teamMembershipsFor(possibleEditee._id) } yield otherIsTeamManagerOrAdmin || teamMemberships.isEmpty + def getMultiUserId(multiUserIdOpt: Option[String], emailOpt: Option[String])( + implicit ctx: DBAccessContext): Fox[ObjectId] = + for { + _ <- Fox.bool2Fox(multiUserIdOpt.isDefined || emailOpt.isDefined) ?~> "user.id.orEmailNecessary" + ownerId <- (emailOpt, multiUserIdOpt) match { + case (_, Some(id)) => Fox.successful(id) + case (Some(email), _) => + for { + owner <- multiUserDAO.findOneByEmail(email) ?~> "user.notFound" + } yield owner._id.toString + case _ => Fox.empty ?~> "user.notFound" + } + id <- ObjectId.fromString(ownerId) + } yield id + + def createUserInOrganization(existingMultiUserId: ObjectId, organizationId: ObjectId, asOwner: Boolean)( + implicit ctx: DBAccessContext): Fox[User] = + for { + _ <- organizationDAO.findOne(organizationId) ?~> "organization.notFound" + _ <- multiUserDAO.findOne(existingMultiUserId) ?~> "user.notFound" + existingUser <- userDAO.findFirstByMultiUser(existingMultiUserId) ?~> "user.notFound" + newUserId = ObjectId.generate + user = User( + newUserId, + existingMultiUserId, + organizationId, + existingUser.firstName, + existingUser.lastName, + Instant.now, + existingUser.userConfiguration, + existingUser.loginInfo, + isAdmin = false, + isOrganizationOwner = asOwner, + isDatasetManager = asOwner, + isDeactivated = false, + isUnlisted = false + ) + _ <- userDAO.insertOne(user) + } yield user + def publicWrites(user: User, requestingUser: User): Fox[JsObject] = { implicit val ctx: DBAccessContext = GlobalAccessContext for { diff --git a/conf/messages b/conf/messages index a7270cbc9b7..4da92ad195e 100644 --- a/conf/messages +++ b/conf/messages @@ -63,6 +63,7 @@ user.notAuthorised=You are not authorized to view this resource. Please log in. user.id.notFound=We could not find a user id in the request. user.id.invalid=The provided user id is invalid. user.creation.failed=Failed to create user +user.id.orEmailNecessary=Email or id needed to identify user. oidc.disabled=OIDC is disabled oidc.configuration.invalid=OIDC configuration is invalid diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index b57ee1e8f56..2fbc32fb413 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -58,6 +58,7 @@ GET /users/:id/tasks GET /users/:id/loggedTime controllers.UserController.userLoggedTime(id: String) POST /users/loggedTime controllers.UserController.usersLoggedTime GET /users/:id/annotations controllers.UserController.userAnnotations(id: String, isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean]) +POST /users/inOrganization/:organizationName controllers.UserController.createInOrganization(organizationName: String) # Team GET /teams controllers.TeamController.list(isEditable: Option[Boolean]) @@ -227,12 +228,14 @@ GET /statistics/users # Organizations GET /organizations controllers.OrganizationController.list +POST /organizations controllers.OrganizationController.create GET /organizations/byInvite/:inviteToken controllers.OrganizationController.getByInvite(inviteToken: String) GET /organizations/default controllers.OrganizationController.getDefault GET /organizationsIsEmpty controllers.OrganizationController.organizationsIsEmpty GET /organizations/:organizationName controllers.OrganizationController.get(organizationName: String) PATCH /organizations/:organizationName controllers.OrganizationController.update(organizationName: String) DELETE /organizations/:organizationName controllers.OrganizationController.delete(organizationName: String) +POST /organizations/:organizationName/addUser controllers.OrganizationController.addUser(organizationName: String) GET /operatorData controllers.OrganizationController.getOperatorData POST /pricing/requestExtension controllers.OrganizationController.sendExtendPricingPlanEmail POST /pricing/requestUpgrade controllers.OrganizationController.sendUpgradePricingPlanEmail(requestedPlan: String) From 80fc81573d07f421c2419a2f748fa0a9d3072fa1 Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 10 Jul 2023 14:43:31 +0200 Subject: [PATCH 02/11] Extract methods --- app/controllers/AuthenticationController.scala | 3 +-- app/controllers/OrganizationController.scala | 6 ++---- app/controllers/UserController.scala | 7 ++----- app/models/user/UserService.scala | 12 +++++++++++- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index 23b569652bd..417b6c1dc31 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -128,8 +128,7 @@ class AuthenticationController @Inject()( password: Option[String], inviteBox: Box[Invite] = Empty, registerBrainDB: Boolean = false)(implicit request: Request[AnyContent]): Fox[User] = { - val passwordInfo: PasswordInfo = - password.map(passwordHasher.hash).getOrElse(userService.getOpenIdConnectPasswordInfo) + val passwordInfo: PasswordInfo = userService.getPasswordInfo(password) for { user <- userService.insert(organization._id, email, diff --git a/app/controllers/OrganizationController.scala b/app/controllers/OrganizationController.scala index 2492cbef9bd..c5cd7c67fd7 100755 --- a/app/controllers/OrganizationController.scala +++ b/app/controllers/OrganizationController.scala @@ -75,8 +75,7 @@ class OrganizationController @Inject()( def create: Action[OrganizationCreationParameters] = sil.SecuredAction.async(validateJson[OrganizationCreationParameters]) { implicit request => for { - isSuperUser <- multiUserDAO.findOne(request.identity._multiUser).map(_.isSuperUser) - _ <- bool2Fox(isSuperUser) ?~> "notAllowed" ~> FORBIDDEN + _ <- userService.assertIsSuperUser(request.identity._multiUser) ?~> "notAllowed" ~> FORBIDDEN ownerId <- userService.getMultiUserId(request.body.ownerId, request.body.ownerEmail) org <- organizationService.createOrganization(request.body.orgName, request.body.orgDisplayName) _ <- userService.createUserInOrganization(ownerId, org._id, asOwner = true) @@ -181,8 +180,7 @@ class OrganizationController @Inject()( def addUser(organizationName: String): Action[AddUserParameters] = sil.SecuredAction.async(validateJson[AddUserParameters]) { implicit request => for { - isSuperUser <- multiUserDAO.findOne(request.identity._multiUser).map(_.isSuperUser) - _ <- bool2Fox(isSuperUser) ?~> "notAllowed" ~> FORBIDDEN + _ <- userService.assertIsSuperUser(request.identity._multiUser) ?~> "notAllowed" ~> FORBIDDEN multiUserId <- userService.getMultiUserId(request.body.multiUserId, request.body.userEmail) organization <- organizationDAO.findOneByName(organizationName) ?~> Messages("organization.notFound", organizationName) ~> NOT_FOUND diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index c0d82638e70..b7b7b6aeeb6 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -475,12 +475,9 @@ class UserController @Inject()(userService: UserService, def createInOrganization(organizationName: String): Action[CreateUserInOrganizationParameters] = sil.SecuredAction.async(validateJson[CreateUserInOrganizationParameters]) { implicit request => for { - isSuperUser <- multiUserDAO.findOne(request.identity._multiUser).map(_.isSuperUser) - _ <- bool2Fox(isSuperUser) ?~> "notAllowed" ~> FORBIDDEN + _ <- userService.assertIsSuperUser(request.identity._multiUser) ?~> "notAllowed" ~> FORBIDDEN organization <- organizationDAO.findOneByName(organizationName) ?~> "organization.notFound" - passwordInfo = request.body.password - .map(passwordHasher.hash) - .getOrElse(userService.getOpenIdConnectPasswordInfo) + passwordInfo = userService.getPasswordInfo(request.body.password) user <- userService.insert(organization._id, request.body.email, request.body.firstName, diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index 513a12a4991..44b810b15f8 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -16,7 +16,7 @@ import com.typesafe.scalalogging.LazyLogging import models.binary.DataSetDAO import models.team._ import oxalis.mail.{DefaultMails, Send} -import oxalis.security.TokenDAO +import oxalis.security.{PasswordHasher, TokenDAO} import play.api.i18n.{Messages, MessagesProvider} import play.api.libs.json._ import utils.{ObjectId, WkConf} @@ -39,6 +39,7 @@ class UserService @Inject()(conf: WkConf, dataSetDAO: DataSetDAO, tokenDAO: TokenDAO, defaultMails: DefaultMails, + passwordHasher: PasswordHasher, actorSystem: ActorSystem)(implicit ec: ExecutionContext) extends FoxImplicits with LazyLogging @@ -81,6 +82,12 @@ class UserService @Inject()(conf: WkConf, _ <- bool2Fox(userBox.isEmpty) ?~> "organization.alreadyJoined" } yield () + def assertIsSuperUser(multiUserId: ObjectId)(implicit ctx: DBAccessContext): Fox[Unit] = + for { + isSuperUser <- multiUserDAO.findOne(multiUserId).map(_.isSuperUser) + _ <- bool2Fox(isSuperUser) + } yield () + def findOneCached(userId: ObjectId)(implicit ctx: DBAccessContext): Fox[User] = userCache.getOrLoad((userId, ctx.toStringAnonymous), _ => userDAO.findOne(userId)) @@ -208,6 +215,9 @@ class UserService @Inject()(conf: WkConf, private def removeUserFromCache(userId: ObjectId): Unit = userCache.clear(idAndAccessContextString => idAndAccessContextString._1 == userId) + def getPasswordInfo(passwordOpt: Option[String]): PasswordInfo = + passwordOpt.map(passwordHasher.hash).getOrElse(getOpenIdConnectPasswordInfo) + def changePasswordInfo(loginInfo: LoginInfo, passwordInfo: PasswordInfo): Fox[PasswordInfo] = for { userIdValidated <- ObjectId.fromString(loginInfo.providerKey) From e8eac92487f691d25efca6dbabb8bff290e5cd29 Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 10 Jul 2023 14:48:22 +0200 Subject: [PATCH 03/11] Update changelog --- CHANGELOG.unreleased.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 8f3ff8ab388..1f4bc6f38eb 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -11,7 +11,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released [Commits](https://github.com/scalableminds/webknossos/compare/23.07.0...HEAD) ### Added -- Added a modal to the voxelytics workflow view that lists all artifacts with their file size and inode count. This helps identifying the largest artifacts to free disk space. [#7152](https://github.com/scalableminds/webknossos/pull/7152) +- Added a modal to the voxelytics workflow view that lists all artifacts with their file size and inode count. This helps to identify the largest artifacts to free disk space. [#7152](https://github.com/scalableminds/webknossos/pull/7152) - In order to facilitate changing the brush size in the brush tool, buttons with preset brush sizes were added. These presets are user configurable by assigning the current brush size to any of the preset buttons. Additionally the preset brush sizes can be set with keyboard shortcuts. [#7101](https://github.com/scalableminds/webknossos/pull/7101) - Added new graphics and restyled empty dashboards. [#7008](https://github.com/scalableminds/webknossos/pull/7008) - Added warning when using WEBKNOSSOS in an outdated browser. [#7165](https://github.com/scalableminds/webknossos/pull/7165) @@ -19,6 +19,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Added support for transformations with thin plate splines. [#7131](https://github.com/scalableminds/webknossos/pull/7131) - WEBKNOSSOS can now read S3 remote dataset credentials from environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_KEY`. Those will be used, if available, when accessing remote datasets for which no explicit credentials are supplied. [#7170](https://github.com/scalableminds/webknossos/pull/7170) - Added security.txt according to [RFC 9116](https://www.rfc-editor.org/rfc/rfc9116). The content is configurable and it can be disabled. [#7182](https://github.com/scalableminds/webknossos/pull/7182) +- Added routes for super-users to manage users and organizations. [#7196](https://github.com/scalableminds/webknossos/pull/7196) ### Changed - Redesigned the info tab in the right-hand sidebar to be fit the new branding and design language. [#7110](https://github.com/scalableminds/webknossos/pull/7110) From 6f720df10f75a8d815d6415185b4bb91121ad7cd Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 17 Jul 2023 11:56:50 +0200 Subject: [PATCH 04/11] Remove POST org, rework create user in org --- .../AuthenticationController.scala | 223 ++++++++++-------- app/controllers/OrganizationController.scala | 17 -- app/controllers/UserController.scala | 26 -- app/oxalis/mail/DefaultMails.scala | 4 +- conf/webknossos.latest.routes | 3 +- 5 files changed, 134 insertions(+), 139 deletions(-) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index 417b6c1dc31..100f5a1519c 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -26,7 +26,7 @@ import oxalis.thirdparty.BrainTracing import play.api.data.Form import play.api.data.Forms.{email, _} import play.api.data.validation.Constraints._ -import play.api.i18n.Messages +import play.api.i18n.{Messages, MessagesProvider} import play.api.libs.json._ import play.api.mvc.{Action, AnyContent, Cookie, PlayBodyParsers, Request, Result} import utils.{ObjectId, WkConf} @@ -75,47 +75,32 @@ class AuthenticationController @Inject()( signUpForm.bindFromRequest.fold( bogusForm => Future.successful(BadRequest(bogusForm.toString)), signUpData => { - val email = signUpData.email.toLowerCase - var errors = List[String]() - val firstName = TextUtils.normalizeStrong(signUpData.firstName).getOrElse { - errors ::= Messages("user.firstName.invalid") - "" - } - val lastName = TextUtils.normalizeStrong(signUpData.lastName).getOrElse { - errors ::= Messages("user.lastName.invalid") - "" - } - multiUserDAO.findOneByEmail(email)(GlobalAccessContext).futureBox.flatMap { - case Full(_) => - errors ::= Messages("user.email.alreadyInUse") - Fox.successful(BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) - case Empty => - if (errors.nonEmpty) { - Fox.successful(BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) - } else { - for { - inviteBox: Box[Invite] <- inviteService.findInviteByTokenOpt(signUpData.inviteToken).futureBox - organizationName = Option(signUpData.organization).filter(_.trim.nonEmpty) - organization <- organizationService.findOneByInviteByNameOrDefault( - inviteBox.toOption, - organizationName)(GlobalAccessContext) ?~> Messages("organization.notFound", signUpData.organization) - _ <- organizationService - .assertUsersCanBeAdded(organization._id)(GlobalAccessContext, ec) ?~> "organization.users.userLimitReached" - autoActivate = inviteBox.toOption.map(_.autoActivate).getOrElse(organization.enableAutoVerify) - _ <- createUser(organization, - email, - firstName, - lastName, - autoActivate, - Option(signUpData.password), - inviteBox, - registerBrainDB = true) - } yield { - Ok - } - } - case f: Failure => Fox.failure(f.msg) + for { + (firstName, lastName, email, errors) <- validateNameAndEmail(signUpData.firstName, + signUpData.lastName, + signUpData.email) + _ <- Fox.bool2Fox(errors.isEmpty) ?~> Json + .obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> Messages(t))))) + .toString() + inviteBox: Box[Invite] <- inviteService.findInviteByTokenOpt(signUpData.inviteToken).futureBox + organizationName = Option(signUpData.organization).filter(_.trim.nonEmpty) + organization <- organizationService.findOneByInviteByNameOrDefault(inviteBox.toOption, organizationName)( + GlobalAccessContext) ?~> Messages("organization.notFound", signUpData.organization) + _ <- organizationService + .assertUsersCanBeAdded(organization._id)(GlobalAccessContext, ec) ?~> "organization.users.userLimitReached" + autoActivate = inviteBox.toOption.map(_.autoActivate).getOrElse(organization.enableAutoVerify) + _ <- createUser(organization, + email, + firstName, + lastName, + autoActivate, + Option(signUpData.password), + inviteBox, + registerBrainDB = true) + } yield { + Ok } + } ) } @@ -127,7 +112,7 @@ class AuthenticationController @Inject()( autoActivate: Boolean, password: Option[String], inviteBox: Box[Invite] = Empty, - registerBrainDB: Boolean = false)(implicit request: Request[AnyContent]): Fox[User] = { + registerBrainDB: Boolean = false)(implicit mp: MessagesProvider): Fox[User] = { val passwordInfo: PasswordInfo = userService.getPasswordInfo(password) for { user <- userService.insert(organization._id, @@ -553,63 +538,117 @@ class AuthenticationController @Inject()( signUpData => { organizationService.assertMayCreateOrganization(request.identity).futureBox.flatMap { case Full(_) => - val email = signUpData.email.toLowerCase - var errors = List[String]() - val firstName = TextUtils.normalizeStrong(signUpData.firstName).getOrElse { - errors ::= Messages("user.firstName.invalid") - "" - } - val lastName = TextUtils.normalizeStrong(signUpData.lastName).getOrElse { - errors ::= Messages("user.lastName.invalid") - "" - } - multiUserDAO.findOneByEmail(email)(GlobalAccessContext).futureBox.flatMap { - case Full(_) => - errors ::= Messages("user.email.alreadyInUse") - Fox.successful(BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) - case Empty => - if (errors.nonEmpty) { - Fox.successful( - BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) - } else { - for { - organization <- organizationService.createOrganization( - Option(signUpData.organization).filter(_.trim.nonEmpty), - signUpData.organizationDisplayName) ?~> "organization.create.failed" - user <- userService.insert(organization._id, - email, - firstName, - lastName, - isActive = true, - passwordHasher.hash(signUpData.password), - isAdmin = true, - isOrganizationOwner = true) ?~> "user.creation.failed" - _ = analyticsService.track(SignupEvent(user, hadInvite = false)) - multiUser <- multiUserDAO.findOne(user._multiUser) - dataStoreToken <- bearerTokenAuthenticatorService - .createAndInit(user.loginInfo, TokenType.DataStore, deleteOld = false) - .toFox - _ <- organizationService - .createOrganizationFolder(organization.name, dataStoreToken) ?~> "organization.folderCreation.failed" - } yield { - Mailer ! Send( - defaultMails.newOrganizationMail(organization.displayName, - email.toLowerCase, - request.headers.get("Host").getOrElse(""))) - if (conf.Features.isWkorgInstance) { - mailchimpClient.registerUser(user, multiUser, MailchimpTag.RegisteredAsAdmin) - } - Ok - } - } - case f: Failure => Fox.failure(f.msg) + for { + (firstName, lastName, email, errors) <- validateNameAndEmail(signUpData.firstName, + signUpData.lastName, + signUpData.email) + _ <- Fox.bool2Fox(errors.isEmpty) ?~> Json + .obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))) + .toString() + organization <- organizationService.createOrganization( + Option(signUpData.organization).filter(_.trim.nonEmpty), + signUpData.organizationDisplayName) ?~> "organization.create.failed" + user <- userService.insert(organization._id, + email, + firstName, + lastName, + isActive = true, + passwordHasher.hash(signUpData.password), + isAdmin = true, + isOrganizationOwner = true) ?~> "user.creation.failed" + _ = analyticsService.track(SignupEvent(user, hadInvite = false)) + multiUser <- multiUserDAO.findOne(user._multiUser) + dataStoreToken <- bearerTokenAuthenticatorService + .createAndInit(user.loginInfo, TokenType.DataStore, deleteOld = false) + .toFox + _ <- organizationService + .createOrganizationFolder(organization.name, dataStoreToken) ?~> "organization.folderCreation.failed" + } yield { + Mailer ! Send(defaultMails + .newOrganizationMail(organization.displayName, email, request.headers.get("Host").getOrElse(""))) + if (conf.Features.isWkorgInstance) { + mailchimpClient.registerUser(user, multiUser, MailchimpTag.RegisteredAsAdmin) + } + Ok } + case _ => Fox.failure(Messages("organization.create.forbidden")) } } ) } + case class CreateUserInOrganizationParameters(firstName: String, + lastName: String, + email: String, + password: Option[String], + autoActivate: Option[Boolean]) + + object CreateUserInOrganizationParameters { + implicit val jsonFormat: OFormat[CreateUserInOrganizationParameters] = + Json.format[CreateUserInOrganizationParameters] + } + + def createUserInOrganization(organizationName: String): Action[CreateUserInOrganizationParameters] = + sil.SecuredAction.async(validateJson[CreateUserInOrganizationParameters]) { implicit request => + for { + _ <- userService.assertIsSuperUser(request.identity._multiUser) ?~> "notAllowed" ~> FORBIDDEN + organization <- organizationDAO.findOneByName(organizationName) ?~> "organization.notFound" + (firstName, lastName, email, errors) <- validateNameAndEmail(request.body.firstName, + request.body.lastName, + request.body.email) + _ <- Fox.bool2Fox(errors.isEmpty) ?~> errors + .map(t => Messages(t)) + .mkString(",") // How to return a BadRequest here with JSON? (see line 84) + user <- createUser(organization, + email, + firstName, + lastName, + request.body.autoActivate.getOrElse(false), + request.body.password, + Empty, + registerBrainDB = true) + } yield { + Ok(user._id.toString) + } + } + + private def validateNameAndEmail(firstName: String, + lastName: String, + email: String): Fox[(String, String, String, List[String])] = { + var (errors, fN, lN) = normalizeName(firstName, lastName) + for { + nameEmailErrorBox: Box[(String, String, String, List[String])] <- multiUserDAO + .findOneByEmail(email.toLowerCase)(GlobalAccessContext) + .futureBox + .flatMap { + case Full(_) => + errors ::= "user.email.alreadyInUse" + Fox.successful(("", "", "", errors)) + case Empty => + if (errors.nonEmpty) { + Fox.successful(("", "", "", errors)) + } else { + Fox.successful((fN, lN, email.toLowerCase, List())) + } + case f: Failure => Fox.failure(f.msg) + } + } yield nameEmailErrorBox + } + + private def normalizeName(firstName: String, lastName: String) = { + var errors = List[String]() + val fN = TextUtils.normalizeStrong(firstName).getOrElse { + errors ::= "user.firstName.invalid" + "" + } + val lN = TextUtils.normalizeStrong(lastName).getOrElse { + errors ::= "user.lastName.invalid" + "" + } + (errors, fN, lN) + } + } case class InviteParameters( diff --git a/app/controllers/OrganizationController.scala b/app/controllers/OrganizationController.scala index c5cd7c67fd7..ce636fac8ad 100755 --- a/app/controllers/OrganizationController.scala +++ b/app/controllers/OrganizationController.scala @@ -65,23 +65,6 @@ class OrganizationController @Inject()( } yield Ok(Json.toJson(js)) } - case class OrganizationCreationParameters(orgName: Option[String], - orgDisplayName: String, - ownerEmail: Option[String], - ownerId: Option[String]) - object OrganizationCreationParameters { - implicit val jsonFormat: OFormat[OrganizationCreationParameters] = Json.format[OrganizationCreationParameters] - } - def create: Action[OrganizationCreationParameters] = - sil.SecuredAction.async(validateJson[OrganizationCreationParameters]) { implicit request => - for { - _ <- userService.assertIsSuperUser(request.identity._multiUser) ?~> "notAllowed" ~> FORBIDDEN - ownerId <- userService.getMultiUserId(request.body.ownerId, request.body.ownerEmail) - org <- organizationService.createOrganization(request.body.orgName, request.body.orgDisplayName) - _ <- userService.createUserInOrganization(ownerId, org._id, asOwner = true) - } yield Ok(org.name) - } - def getDefault: Action[AnyContent] = Action.async { implicit request => for { allOrgs <- organizationDAO.findAll(GlobalAccessContext) ?~> "organization.list.failed" diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index b7b7b6aeeb6..5c6ae59b53f 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -463,30 +463,4 @@ class UserController @Inject()(userService: UserService, } yield Ok(updatedJs) } - case class CreateUserInOrganizationParameters(firstName: String, - lastName: String, - email: String, - password: Option[String]) - - object CreateUserInOrganizationParameters { - implicit val jsonFormat: OFormat[CreateUserInOrganizationParameters] = - Json.format[CreateUserInOrganizationParameters] - } - def createInOrganization(organizationName: String): Action[CreateUserInOrganizationParameters] = - sil.SecuredAction.async(validateJson[CreateUserInOrganizationParameters]) { implicit request => - for { - _ <- userService.assertIsSuperUser(request.identity._multiUser) ?~> "notAllowed" ~> FORBIDDEN - organization <- organizationDAO.findOneByName(organizationName) ?~> "organization.notFound" - passwordInfo = userService.getPasswordInfo(request.body.password) - user <- userService.insert(organization._id, - request.body.email, - request.body.firstName, - request.body.lastName, - isActive = true, - passwordInfo, - isAdmin = false, - isOrganizationOwner = false) - } yield Ok(user._id.toString) - } - } diff --git a/app/oxalis/mail/DefaultMails.scala b/app/oxalis/mail/DefaultMails.scala index d0bb673bfab..91f3149b124 100755 --- a/app/oxalis/mail/DefaultMails.scala +++ b/app/oxalis/mail/DefaultMails.scala @@ -2,7 +2,7 @@ package oxalis.mail import models.organization.Organization import models.user.User -import play.api.i18n.Messages +import play.api.i18n.{Messages, MessagesProvider} import utils.WkConf import views._ @@ -42,7 +42,7 @@ class DefaultMails @Inject()(conf: WkConf) { ) def newUserMail(name: String, receiver: String, brainDBresult: Option[String], enableAutoVerify: Boolean)( - implicit messages: Messages): Mail = + implicit mp: MessagesProvider): Mail = Mail( from = defaultSender, subject = "Welcome to WEBKNOSSOS", diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index 2fbc32fb413..b8586a84ae3 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -31,6 +31,7 @@ GET /auth/oidc/login # /auth/oidc/callback route is used literally in code GET /auth/oidc/callback controllers.AuthenticationController.openIdCallback POST /auth/createOrganizationWithAdmin controllers.AuthenticationController.createOrganizationWithAdmin +POST /auth/createUserInOrganization/:organizationName controllers.AuthenticationController.createUserInOrganization(organizationName: String) # Configurations GET /user/userConfiguration controllers.ConfigurationController.read @@ -58,7 +59,6 @@ GET /users/:id/tasks GET /users/:id/loggedTime controllers.UserController.userLoggedTime(id: String) POST /users/loggedTime controllers.UserController.usersLoggedTime GET /users/:id/annotations controllers.UserController.userAnnotations(id: String, isFinished: Option[Boolean], limit: Option[Int], pageNumber: Option[Int], includeTotalCount: Option[Boolean]) -POST /users/inOrganization/:organizationName controllers.UserController.createInOrganization(organizationName: String) # Team GET /teams controllers.TeamController.list(isEditable: Option[Boolean]) @@ -228,7 +228,6 @@ GET /statistics/users # Organizations GET /organizations controllers.OrganizationController.list -POST /organizations controllers.OrganizationController.create GET /organizations/byInvite/:inviteToken controllers.OrganizationController.getByInvite(inviteToken: String) GET /organizations/default controllers.OrganizationController.getDefault GET /organizationsIsEmpty controllers.OrganizationController.organizationsIsEmpty From 14c193a00a23d37ad75e61af225f1d76035e9c2f Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 17 Jul 2023 13:15:29 +0200 Subject: [PATCH 05/11] Simplify adding user to organization --- app/controllers/OrganizationController.scala | 16 +++----- app/models/user/UserService.scala | 40 -------------------- 2 files changed, 6 insertions(+), 50 deletions(-) diff --git a/app/controllers/OrganizationController.scala b/app/controllers/OrganizationController.scala index ce636fac8ad..fe882485530 100755 --- a/app/controllers/OrganizationController.scala +++ b/app/controllers/OrganizationController.scala @@ -13,7 +13,7 @@ import models.team.PricingPlan import oxalis.security.{WkEnv, WkSilhouetteEnvironment} import play.api.i18n.Messages import play.api.libs.functional.syntax._ -import play.api.libs.json.{JsNull, JsValue, Json, OFormat, __} +import play.api.libs.json.{JsNull, JsValue, Json, __} import play.api.mvc.{Action, AnyContent, PlayBodyParsers} import utils.WkConf @@ -155,19 +155,15 @@ class OrganizationController @Inject()( } yield Ok } - case class AddUserParameters(userEmail: Option[String], multiUserId: Option[String]) - object AddUserParameters { - implicit val jsonFormat: OFormat[AddUserParameters] = Json.format[AddUserParameters] - } - - def addUser(organizationName: String): Action[AddUserParameters] = - sil.SecuredAction.async(validateJson[AddUserParameters]) { implicit request => + def addUser(organizationName: String): Action[String] = + sil.SecuredAction.async(validateJson[String]) { implicit request => for { _ <- userService.assertIsSuperUser(request.identity._multiUser) ?~> "notAllowed" ~> FORBIDDEN - multiUserId <- userService.getMultiUserId(request.body.multiUserId, request.body.userEmail) + multiUser <- multiUserDAO.findOneByEmail(request.body) organization <- organizationDAO.findOneByName(organizationName) ?~> Messages("organization.notFound", organizationName) ~> NOT_FOUND - user <- userService.createUserInOrganization(multiUserId, organization._id, asOwner = false) + user <- userDAO.findFirstByMultiUser(multiUser._id) + user <- userService.joinOrganization(user, organization._id, autoActivate = true, isAdmin = false) } yield Ok(user._id.toString) } diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index 44b810b15f8..5446f5a2ae2 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -327,46 +327,6 @@ class UserService @Inject()(conf: WkConf, teamMemberships <- teamMembershipsFor(possibleEditee._id) } yield otherIsTeamManagerOrAdmin || teamMemberships.isEmpty - def getMultiUserId(multiUserIdOpt: Option[String], emailOpt: Option[String])( - implicit ctx: DBAccessContext): Fox[ObjectId] = - for { - _ <- Fox.bool2Fox(multiUserIdOpt.isDefined || emailOpt.isDefined) ?~> "user.id.orEmailNecessary" - ownerId <- (emailOpt, multiUserIdOpt) match { - case (_, Some(id)) => Fox.successful(id) - case (Some(email), _) => - for { - owner <- multiUserDAO.findOneByEmail(email) ?~> "user.notFound" - } yield owner._id.toString - case _ => Fox.empty ?~> "user.notFound" - } - id <- ObjectId.fromString(ownerId) - } yield id - - def createUserInOrganization(existingMultiUserId: ObjectId, organizationId: ObjectId, asOwner: Boolean)( - implicit ctx: DBAccessContext): Fox[User] = - for { - _ <- organizationDAO.findOne(organizationId) ?~> "organization.notFound" - _ <- multiUserDAO.findOne(existingMultiUserId) ?~> "user.notFound" - existingUser <- userDAO.findFirstByMultiUser(existingMultiUserId) ?~> "user.notFound" - newUserId = ObjectId.generate - user = User( - newUserId, - existingMultiUserId, - organizationId, - existingUser.firstName, - existingUser.lastName, - Instant.now, - existingUser.userConfiguration, - existingUser.loginInfo, - isAdmin = false, - isOrganizationOwner = asOwner, - isDatasetManager = asOwner, - isDeactivated = false, - isUnlisted = false - ) - _ <- userDAO.insertOne(user) - } yield user - def publicWrites(user: User, requestingUser: User): Fox[JsObject] = { implicit val ctx: DBAccessContext = GlobalAccessContext for { From 8072d7ddcad10dd6c6e9d041cbaa1c3c375233cc Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 17 Jul 2023 13:46:48 +0200 Subject: [PATCH 06/11] Return multiple errors --- .../AuthenticationController.scala | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index 100f5a1519c..09ee1589774 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -79,26 +79,33 @@ class AuthenticationController @Inject()( (firstName, lastName, email, errors) <- validateNameAndEmail(signUpData.firstName, signUpData.lastName, signUpData.email) - _ <- Fox.bool2Fox(errors.isEmpty) ?~> Json - .obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> Messages(t))))) - .toString() - inviteBox: Box[Invite] <- inviteService.findInviteByTokenOpt(signUpData.inviteToken).futureBox - organizationName = Option(signUpData.organization).filter(_.trim.nonEmpty) - organization <- organizationService.findOneByInviteByNameOrDefault(inviteBox.toOption, organizationName)( - GlobalAccessContext) ?~> Messages("organization.notFound", signUpData.organization) - _ <- organizationService - .assertUsersCanBeAdded(organization._id)(GlobalAccessContext, ec) ?~> "organization.users.userLimitReached" - autoActivate = inviteBox.toOption.map(_.autoActivate).getOrElse(organization.enableAutoVerify) - _ <- createUser(organization, - email, - firstName, - lastName, - autoActivate, - Option(signUpData.password), - inviteBox, - registerBrainDB = true) + result <- if (errors.nonEmpty) { + Fox.successful(BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) + } else { + for { + _ <- Fox.bool2Fox(errors.isEmpty) ?~> Json + .obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> Messages(t))))) + .toString() + + inviteBox: Box[Invite] <- inviteService.findInviteByTokenOpt(signUpData.inviteToken).futureBox + organizationName = Option(signUpData.organization).filter(_.trim.nonEmpty) + organization <- organizationService.findOneByInviteByNameOrDefault(inviteBox.toOption, organizationName)( + GlobalAccessContext) ?~> Messages("organization.notFound", signUpData.organization) + _ <- organizationService + .assertUsersCanBeAdded(organization._id)(GlobalAccessContext, ec) ?~> "organization.users.userLimitReached" + autoActivate = inviteBox.toOption.map(_.autoActivate).getOrElse(organization.enableAutoVerify) + _ <- createUser(organization, + email, + firstName, + lastName, + autoActivate, + Option(signUpData.password), + inviteBox, + registerBrainDB = true) + } yield Ok + } } yield { - Ok + result } } @@ -597,19 +604,20 @@ class AuthenticationController @Inject()( (firstName, lastName, email, errors) <- validateNameAndEmail(request.body.firstName, request.body.lastName, request.body.email) - _ <- Fox.bool2Fox(errors.isEmpty) ?~> errors - .map(t => Messages(t)) - .mkString(",") // How to return a BadRequest here with JSON? (see line 84) - user <- createUser(organization, - email, - firstName, - lastName, - request.body.autoActivate.getOrElse(false), - request.body.password, - Empty, - registerBrainDB = true) + result <- if (errors.isEmpty) { + createUser(organization, + email, + firstName, + lastName, + request.body.autoActivate.getOrElse(false), + request.body.password, + Empty, + registerBrainDB = true).map(u => Ok(u._id.toString)) + } else { + Fox.successful(BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) + } } yield { - Ok(user._id.toString) + result } } From a759dc114259fcc99c03a82c39611ab7691cf2db Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 17 Jul 2023 13:50:02 +0200 Subject: [PATCH 07/11] Remove superflous line --- app/controllers/AuthenticationController.scala | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index 09ee1589774..cda5ad7c2f2 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -82,11 +82,7 @@ class AuthenticationController @Inject()( result <- if (errors.nonEmpty) { Fox.successful(BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) } else { - for { - _ <- Fox.bool2Fox(errors.isEmpty) ?~> Json - .obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> Messages(t))))) - .toString() - + for {g inviteBox: Box[Invite] <- inviteService.findInviteByTokenOpt(signUpData.inviteToken).futureBox organizationName = Option(signUpData.organization).filter(_.trim.nonEmpty) organization <- organizationService.findOneByInviteByNameOrDefault(inviteBox.toOption, organizationName)( From e0f222cfd7982de88b7dcf877f12a5538c647d59 Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 17 Jul 2023 13:53:00 +0200 Subject: [PATCH 08/11] Update createorgwithadmin --- .../AuthenticationController.scala | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index cda5ad7c2f2..dee8bb13781 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -82,7 +82,7 @@ class AuthenticationController @Inject()( result <- if (errors.nonEmpty) { Fox.successful(BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) } else { - for {g + for { inviteBox: Box[Invite] <- inviteService.findInviteByTokenOpt(signUpData.inviteToken).futureBox organizationName = Option(signUpData.organization).filter(_.trim.nonEmpty) organization <- organizationService.findOneByInviteByNameOrDefault(inviteBox.toOption, organizationName)( @@ -545,36 +545,38 @@ class AuthenticationController @Inject()( (firstName, lastName, email, errors) <- validateNameAndEmail(signUpData.firstName, signUpData.lastName, signUpData.email) - _ <- Fox.bool2Fox(errors.isEmpty) ?~> Json - .obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))) - .toString() - organization <- organizationService.createOrganization( - Option(signUpData.organization).filter(_.trim.nonEmpty), - signUpData.organizationDisplayName) ?~> "organization.create.failed" - user <- userService.insert(organization._id, - email, - firstName, - lastName, - isActive = true, - passwordHasher.hash(signUpData.password), - isAdmin = true, - isOrganizationOwner = true) ?~> "user.creation.failed" - _ = analyticsService.track(SignupEvent(user, hadInvite = false)) - multiUser <- multiUserDAO.findOne(user._multiUser) - dataStoreToken <- bearerTokenAuthenticatorService - .createAndInit(user.loginInfo, TokenType.DataStore, deleteOld = false) - .toFox - _ <- organizationService - .createOrganizationFolder(organization.name, dataStoreToken) ?~> "organization.folderCreation.failed" - } yield { - Mailer ! Send(defaultMails - .newOrganizationMail(organization.displayName, email, request.headers.get("Host").getOrElse(""))) - if (conf.Features.isWkorgInstance) { - mailchimpClient.registerUser(user, multiUser, MailchimpTag.RegisteredAsAdmin) + result <- if (errors.nonEmpty) { + Fox.successful(BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) + } else { + for { + organization <- organizationService.createOrganization( + Option(signUpData.organization).filter(_.trim.nonEmpty), + signUpData.organizationDisplayName) ?~> "organization.create.failed" + user <- userService.insert(organization._id, + email, + firstName, + lastName, + isActive = true, + passwordHasher.hash(signUpData.password), + isAdmin = true, + isOrganizationOwner = true) ?~> "user.creation.failed" + _ = analyticsService.track(SignupEvent(user, hadInvite = false)) + multiUser <- multiUserDAO.findOne(user._multiUser) + dataStoreToken <- bearerTokenAuthenticatorService + .createAndInit(user.loginInfo, TokenType.DataStore, deleteOld = false) + .toFox + _ <- organizationService + .createOrganizationFolder(organization.name, dataStoreToken) ?~> "organization.folderCreation.failed" + } yield { + Mailer ! Send(defaultMails + .newOrganizationMail(organization.displayName, email, request.headers.get("Host").getOrElse(""))) + if (conf.Features.isWkorgInstance) { + mailchimpClient.registerUser(user, multiUser, MailchimpTag.RegisteredAsAdmin) + } + Ok + } } - Ok - } - + } yield result case _ => Fox.failure(Messages("organization.create.forbidden")) } } From dca2c55b24a4109c1ada9afb10b4ee6e481cf6c0 Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 17 Jul 2023 14:12:45 +0200 Subject: [PATCH 09/11] Fix a little thing --- app/controllers/AuthenticationController.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index dee8bb13781..ab8c57c20e7 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -83,6 +83,7 @@ class AuthenticationController @Inject()( Fox.successful(BadRequest(Json.obj("messages" -> Json.toJson(errors.map(t => Json.obj("error" -> t)))))) } else { for { + _ <- Fox.successful(()) inviteBox: Box[Invite] <- inviteService.findInviteByTokenOpt(signUpData.inviteToken).futureBox organizationName = Option(signUpData.organization).filter(_.trim.nonEmpty) organization <- organizationService.findOneByInviteByNameOrDefault(inviteBox.toOption, organizationName)( From 32f41a9db0f92a9473fffe2fbb7d46e9d4769f85 Mon Sep 17 00:00:00 2001 From: frcroth Date: Tue, 18 Jul 2023 10:04:27 +0200 Subject: [PATCH 10/11] Readd organization creation route --- app/controllers/OrganizationController.scala | 19 ++++++++++++++++++- conf/webknossos.latest.routes | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/controllers/OrganizationController.scala b/app/controllers/OrganizationController.scala index fe882485530..80d2d09e13c 100755 --- a/app/controllers/OrganizationController.scala +++ b/app/controllers/OrganizationController.scala @@ -13,7 +13,7 @@ import models.team.PricingPlan import oxalis.security.{WkEnv, WkSilhouetteEnvironment} import play.api.i18n.Messages import play.api.libs.functional.syntax._ -import play.api.libs.json.{JsNull, JsValue, Json, __} +import play.api.libs.json.{JsNull, JsValue, Json, OFormat, __} import play.api.mvc.{Action, AnyContent, PlayBodyParsers} import utils.WkConf @@ -65,6 +65,23 @@ class OrganizationController @Inject()( } yield Ok(Json.toJson(js)) } + case class OrganizationCreationParameters(organization: Option[String], + organizationDisplayName: String, + ownerEmail: String) + object OrganizationCreationParameters { + implicit val jsonFormat: OFormat[OrganizationCreationParameters] = Json.format[OrganizationCreationParameters] + } + def create: Action[OrganizationCreationParameters] = + sil.SecuredAction.async(validateJson[OrganizationCreationParameters]) { implicit request => + for { + _ <- userService.assertIsSuperUser(request.identity._multiUser) ?~> "notAllowed" ~> FORBIDDEN + owner <- multiUserDAO.findOneByEmail(request.body.ownerEmail) ?~> "user.notFound" + org <- organizationService.createOrganization(request.body.organization, request.body.organizationDisplayName) + user <- userDAO.findFirstByMultiUser(owner._id) + _ <- userService.joinOrganization(user, org._id, autoActivate = true, isAdmin = true) + } yield Ok(org.name) + } + def getDefault: Action[AnyContent] = Action.async { implicit request => for { allOrgs <- organizationDAO.findAll(GlobalAccessContext) ?~> "organization.list.failed" diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index b8586a84ae3..9907c017f25 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -228,6 +228,7 @@ GET /statistics/users # Organizations GET /organizations controllers.OrganizationController.list +POST /organizations controllers.OrganizationController.create GET /organizations/byInvite/:inviteToken controllers.OrganizationController.getByInvite(inviteToken: String) GET /organizations/default controllers.OrganizationController.getDefault GET /organizationsIsEmpty controllers.OrganizationController.organizationsIsEmpty From 0843063d9a0983ce324dfdd7cb1fdb8d8000c1a3 Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 24 Jul 2023 11:48:19 +0200 Subject: [PATCH 11/11] Apply suggestions from code review --- app/controllers/OrganizationController.scala | 6 +++++- app/models/user/UserService.scala | 10 ++++------ conf/messages | 1 - 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/controllers/OrganizationController.scala b/app/controllers/OrganizationController.scala index 80d2d09e13c..189b65d0dbe 100755 --- a/app/controllers/OrganizationController.scala +++ b/app/controllers/OrganizationController.scala @@ -78,7 +78,11 @@ class OrganizationController @Inject()( owner <- multiUserDAO.findOneByEmail(request.body.ownerEmail) ?~> "user.notFound" org <- organizationService.createOrganization(request.body.organization, request.body.organizationDisplayName) user <- userDAO.findFirstByMultiUser(owner._id) - _ <- userService.joinOrganization(user, org._id, autoActivate = true, isAdmin = true) + _ <- userService.joinOrganization(user, + org._id, + autoActivate = true, + isAdmin = true, + isOrganizationOwner = true) } yield Ok(org.name) } diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index 5446f5a2ae2..ca77ad0f507 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -83,10 +83,7 @@ class UserService @Inject()(conf: WkConf, } yield () def assertIsSuperUser(multiUserId: ObjectId)(implicit ctx: DBAccessContext): Fox[Unit] = - for { - isSuperUser <- multiUserDAO.findOne(multiUserId).map(_.isSuperUser) - _ <- bool2Fox(isSuperUser) - } yield () + Fox.assertTrue(multiUserDAO.findOne(multiUserId).map(_.isSuperUser)) def findOneCached(userId: ObjectId)(implicit ctx: DBAccessContext): Fox[User] = userCache.getOrLoad((userId, ctx.toStringAnonymous), _ => userDAO.findOne(userId)) @@ -149,7 +146,8 @@ class UserService @Inject()(conf: WkConf, organizationId: ObjectId, autoActivate: Boolean, isAdmin: Boolean = false, - isUnlisted: Boolean = false): Fox[User] = + isUnlisted: Boolean = false, + isOrganizationOwner: Boolean = false): Fox[User] = for { newUserId <- Fox.successful(ObjectId.generate) organizationTeamId <- organizationDAO.findOrganizationTeamId(organizationId) @@ -164,7 +162,7 @@ class UserService @Inject()(conf: WkConf, isDatasetManager = false, isDeactivated = !autoActivate, lastTaskTypeId = None, - isOrganizationOwner = false, + isOrganizationOwner = isOrganizationOwner, isUnlisted = isUnlisted, created = Instant.now ) diff --git a/conf/messages b/conf/messages index 4da92ad195e..a7270cbc9b7 100644 --- a/conf/messages +++ b/conf/messages @@ -63,7 +63,6 @@ user.notAuthorised=You are not authorized to view this resource. Please log in. user.id.notFound=We could not find a user id in the request. user.id.invalid=The provided user id is invalid. user.creation.failed=Failed to create user -user.id.orEmailNecessary=Email or id needed to identify user. oidc.disabled=OIDC is disabled oidc.configuration.invalid=OIDC configuration is invalid