diff --git a/app/controllers/ApplicationController.scala b/app/controllers/ApplicationController.scala index 61085a1e1..75fadef10 100644 --- a/app/controllers/ApplicationController.scala +++ b/app/controllers/ApplicationController.scala @@ -55,7 +55,8 @@ case class ApplicationController @Inject() ( extends InjectedController with play.api.i18n.I18nSupport with Operators.Common - with Operators.ApplicationOperators { + with Operators.ApplicationOperators + with Operators.UserOperators { private val filesPath = configuration.underlying.getString("app.filesPath") private val featureMandatSms: Boolean = configuration.get[Boolean]("app.features.smsMandat") @@ -512,16 +513,14 @@ case class ApplicationController @Inject() ( applications <- applicationService.allForUserIds(users.map(_.id)) } yield (users, applications) - private def generateStats[A]( + private def generateStats( areaIds: List[UUID], organisationIds: List[Organisation.Id], groupIds: List[UUID], creationMinDate: LocalDate, - creationMaxDate: LocalDate - )(implicit - webJarsUtil: org.webjars.play.WebJarsUtil, - request: RequestWithUserData[A] - ): Future[Html] = { + creationMaxDate: LocalDate, + rights: Authorization.UserRights + )(implicit webJarsUtil: WebJarsUtil): Future[Html] = { val usersAndApplications: Future[(List[User], List[Application])] = (areaIds, organisationIds, groupIds) match { case (Nil, Nil, Nil) => @@ -592,7 +591,7 @@ case class ApplicationController @Inject() ( users <- usersAndApplications.map { case (users, _) => users } applications <- applicationsFuture relatedUsers <- relatedUsersFuture - } yield views.html.stats.charts(Authorization.isAdmin(request.rights))( + } yield views.html.stats.charts(Authorization.isAdmin(rights))( statsAggregates(applications, relatedUsers), users ) @@ -615,93 +614,97 @@ case class ApplicationController @Inject() ( def stats: Action[AnyContent] = loginAction.async { implicit request => - // TODO: remove `.get` - val (areaIds, organisationIds, groupIds, creationMinDate, creationMaxDate) = - statsForm.bindFromRequest().value.get + statsPage(routes.ApplicationController.stats, request.currentUser, request.rights) + } - val observableOrganisationIds = if (Authorization.isAdmin(request.rights)) { - organisationIds - } else { - organisationIds.filter(id => Authorization.canObserveOrganisation(id)(request.rights)) + def statsAs(otherUserId: UUID): Action[AnyContent] = + loginAction.async { implicit request => + withUser(otherUserId) { otherUser: User => + asUserWithAuthorization(Authorization.canSeeOtherUserNonPrivateViews(otherUser))( + () => + EventType.MasqueradeUnauthorized -> s"Accès non autorisé pour voir la page stats de $otherUserId", + errorInvolvesUser = Some(otherUser.id) + ) { () => + LoginAction.readUserRights(otherUser).flatMap { userRights => + statsPage(routes.ApplicationController.statsAs(otherUserId), otherUser, userRights) + } + } } + } - val observableGroupIds = if (Authorization.isAdmin(request.rights)) { - groupIds - } else { - groupIds.intersect(request.currentUser.groupIds) - } + private def statsPage(formUrl: Call, user: User, rights: Authorization.UserRights)(implicit + request: RequestWithUserData[_] + ): Future[Result] = { + // TODO: remove `.get` + val (areaIds, organisationIds, groupIds, creationMinDate, creationMaxDate) = + statsForm.bindFromRequest().value.get - val cacheKey = - Authorization.isAdmin(request.rights).toString + - ".stats." + - Hash.sha256( - areaIds.toString + observableOrganisationIds.toString + observableGroupIds.toString + - creationMinDate.toString + creationMaxDate.toString - ) + val observableOrganisationIds = if (Authorization.isAdmin(rights)) { + organisationIds + } else { + organisationIds.filter(id => Authorization.canObserveOrganisation(id)(rights)) + } + + val observableGroupIds = if (Authorization.isAdmin(rights)) { + groupIds + } else { + groupIds.intersect(user.groupIds) + } - userGroupService.byIdsFuture(request.currentUser.groupIds).flatMap { currentUserGroups => - cache - .getOrElseUpdate[Html](cacheKey, 1.hours)( - generateStats( + val cacheKey = + Authorization.isAdmin(rights).toString + + ".stats." + + Hash.sha256( + areaIds.toString + observableOrganisationIds.toString + observableGroupIds.toString + + creationMinDate.toString + creationMaxDate.toString + ) + + userGroupService.byIdsFuture(user.groupIds).flatMap { currentUserGroups => + cache + .getOrElseUpdate[Html](cacheKey, 1.hours)( + generateStats( + areaIds, + observableOrganisationIds, + observableGroupIds, + creationMinDate, + creationMaxDate, + rights + ) + ) + .map { html => + eventService.log(StatsShowed, "Visualise les stats") + Ok( + views.html.stats.page(user, rights)( + formUrl, + html, + groupsThatCanBeFilteredBy = currentUserGroups, areaIds, - observableOrganisationIds, - observableGroupIds, + organisationIds, + groupIds, creationMinDate, creationMaxDate ) - ) - .map { html => - eventService.log(StatsShowed, "Visualise les stats") - Ok( - views.html.stats.page(request.currentUser, request.rights)( - html, - groupsThatCanBeFilteredBy = currentUserGroups, - areaIds, - organisationIds, - groupIds, - creationMinDate, - creationMaxDate - ) - ).withHeaders(CONTENT_SECURITY_POLICY -> statsCSP) - } - } + ).withHeaders(CONTENT_SECURITY_POLICY -> statsCSP) + } } + } def allAs(userId: UUID): Action[AnyContent] = loginAction.async { implicit request => - val userOption = userService.byId(userId) - (request.currentUser.admin, userOption) match { - case (false, Some(user)) => - eventService.log( - AllAsUnauthorized, - s"L'utilisateur n'a pas de droit d'afficher la vue de l'utilisateur $userId", - involvesUser = Some(user.id) - ) - Future( - Unauthorized( - s"Vous n'avez pas le droit de faire ça, vous n'êtes pas administrateur. Vous pouvez contacter l'équipe A+ : ${Constants.supportEmail}" - ) - ) - case (true, Some(user)) if user.admin => - eventService.log( - AllAsUnauthorized, - s"L'utilisateur n'a pas de droit d'afficher la vue de l'utilisateur admin $userId", - involvesUser = Some(user.id) - ) - Future( - Unauthorized( - s"Vous n'avez pas le droit de faire ça avec un compte administrateur. Vous pouvez contacter l'équipe A+ : ${Constants.supportEmail}" - ) - ) - case (true, Some(user)) if request.currentUser.areas.intersect(user.areas).nonEmpty => - LoginAction.readUserRights(user).map { userRights => - val targetUserId = user.id + withUser(userId) { otherUser: User => + asUserWithAuthorization(Authorization.canSeeOtherUserNonPrivateViews(otherUser))( + () => + EventType.AllAsUnauthorized -> s"Accès non autorisé pour voir la liste des demandes de $userId", + errorInvolvesUser = Some(otherUser.id) + ) { () => + LoginAction.readUserRights(otherUser).map { userRights => + val targetUserId = otherUser.id val applicationsFromTheArea = List.empty[Application] eventService .log( AllAsShowed, s"Visualise la vue de l'utilisateur $userId", - involvesUser = Some(user.id) + involvesUser = Some(otherUser.id) ) val applications = applicationService.allForUserId( userId = targetUserId, @@ -709,20 +712,14 @@ case class ApplicationController @Inject() ( ) val (closedApplications, openApplications) = applications.partition(_.closed) Ok( - views.html.myApplications(user, userRights)( + views.html.myApplications(otherUser, userRights)( myOpenApplications = openApplications, myClosedApplications = closedApplications, applicationsFromTheArea = applicationsFromTheArea ) ) } - case _ => - eventService.log(AllAsNotFound, s"L'utilisateur $userId n'existe pas") - Future( - BadRequest( - s"L'utilisateur n'existe pas ou vous n'avez pas le droit d'accéder à cette page. Vous pouvez contacter l'équipe A+ : ${Constants.supportEmail}" - ) - ) + } } } diff --git a/app/controllers/GroupController.scala b/app/controllers/GroupController.scala index ffd117a50..544ed6b53 100644 --- a/app/controllers/GroupController.scala +++ b/app/controllers/GroupController.scala @@ -78,7 +78,7 @@ case class GroupController @Inject() ( val message = "L’adresse email n'est pas correcte" eventService.log(EventType.AddUserToGroupBadUserInput, message) if (originIsGroupPage) withGroup(groupId)(group => editGroupPage(group, err)) - else editMyGroupsPage(err) + else editMyGroupsPage(request.currentUser, request.rights, err) }, data => userService @@ -130,7 +130,7 @@ case class GroupController @Inject() ( withUser( userId, includeDisabled = true, - errorMessage = s"L'utilisateur $userId n'existe pas et ne peut pas être réactivé", + errorMessage = s"L'utilisateur $userId n'existe pas et ne peut pas être réactivé".some, errorResult = Redirect(redirectPage) .flashing( "error" -> ("L’utilisateur n’existe pas dans Administration+. " + @@ -173,7 +173,7 @@ case class GroupController @Inject() ( withUser( userId, includeDisabled = true, - errorMessage = s"L'utilisateur $userId n'existe pas et ne peut pas être désactivé", + errorMessage = s"L'utilisateur $userId n'existe pas et ne peut pas être désactivé".some, errorResult = Redirect(redirectPage) .flashing( "error" -> ("L’utilisateur n’existe pas dans Administration+. " + @@ -218,9 +218,24 @@ case class GroupController @Inject() ( } } - def showEditMyGroups = + def showEditMyGroups: Action[AnyContent] = loginAction.async { implicit request => - editMyGroupsPage(AddUserToGroupFormData.form) + editMyGroupsPage(request.currentUser, request.rights, AddUserToGroupFormData.form) + } + + def showEditMyGroupsAs(otherUserId: UUID): Action[AnyContent] = + loginAction.async { implicit request => + withUser(otherUserId) { otherUser: User => + asUserWithAuthorization(Authorization.canSeeOtherUserNonPrivateViews(otherUser))( + () => + EventType.MasqueradeUnauthorized -> s"Accès non autorisé pour voir la page mes groupes de $otherUserId", + errorInvolvesUser = Some(otherUser.id) + ) { () => + LoginAction.readUserRights(otherUser).flatMap { userRights => + editMyGroupsPage(otherUser, userRights, AddUserToGroupFormData.form) + } + } + } } def deleteUnusedGroupById(groupId: UUID): Action[AnyContent] = @@ -387,10 +402,12 @@ case class GroupController @Inject() ( } private def editMyGroupsPage( + user: User, + rights: Authorization.UserRights, addUserForm: Form[AddUserToGroupFormData] )(implicit request: RequestWithUserData[_]): Future[Result] = for { - groups <- groupService.byIdsFuture(request.currentUser.groupIds) + groups <- groupService.byIdsFuture(user.groupIds) users <- userService.byGroupIdsFuture(groups.map(_.id), includeDisabled = true) applications <- applicationService.allForUserIds(users.map(_.id)) } yield { @@ -398,8 +415,8 @@ case class GroupController @Inject() ( Ok( views.editMyGroups .page( - request.currentUser, - request.rights, + user, + rights, addUserForm, groups, users, diff --git a/app/controllers/Operators.scala b/app/controllers/Operators.scala index b4dedea84..08de2fa33 100644 --- a/app/controllers/Operators.scala +++ b/app/controllers/Operators.scala @@ -80,7 +80,7 @@ object Operators { def withUser( userId: UUID, includeDisabled: Boolean = false, - errorMessage: String = "Tentative d'accès à un utilisateur inexistant", + errorMessage: Option[String] = none, errorResult: Option[Result] = none )( payload: User => Future[Result] @@ -88,15 +88,21 @@ object Operators { userService .byId(userId, includeDisabled) .fold({ - eventService.log(UserNotFound, description = errorMessage) + eventService.log( + UserNotFound, + description = + errorMessage.getOrElse(s"Tentative d'accès à un utilisateur inexistant ($userId)"), + involvesUser = Some(userId) + ) Future.successful( - errorResult.getOrElse(NotFound("Utilisateur inexistant")) + errorResult.getOrElse(NotFound(s"L'utilisateur n'existe pas.")) ) })({ user: User => payload(user) }) def asUserWithAuthorization(authorizationCheck: Authorization.Check)( errorEvent: () => (EventType, String), - errorResult: Option[Result] = none + errorResult: Option[Result] = none, + errorInvolvesUser: Option[UUID] = none, )( payload: () => Future[Result] )(implicit request: RequestWithUserData[_]): Future[Result] = @@ -104,7 +110,7 @@ object Operators { payload() } else { val (eventType, description) = errorEvent() - eventService.log(eventType, description = description) + eventService.log(eventType, description = description, involvesUser = errorInvolvesUser) Future.successful( errorResult.getOrElse(Unauthorized("Vous n'avez pas le droit de faire ça")) ) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index d202ffce4..c7a637887 100644 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -360,26 +360,26 @@ case class UserController @Inject() ( asUserWithAuthorization(Authorization.canSeeEditUserPage) { () => ViewUserUnauthorized -> s"Accès non autorisé pour voir $userId" } { () => - userService.byId(userId, includeDisabled = true) match { - case None => - eventService.log(UserNotFound, s"L'utilisateur $userId n'existe pas") - Future(NotFound("Nous n'avons pas trouvé cet utilisateur")) - case Some(user) if Authorization.canSeeOtherUser(user)(request.rights) => - val form = EditUserFormData.form.fill(EditUserFormData.fromUser(user)) + withUser(userId, includeDisabled = true) { otherUser: User => + asUserWithAuthorization(Authorization.canSeeOtherUser(otherUser))( + () => ViewUserUnauthorized -> s"Accès non autorisé pour voir $userId", + errorInvolvesUser = otherUser.id.some + ) { () => + val form = EditUserFormData.form.fill(EditUserFormData.fromUser(otherUser)) val groups = groupService.allGroups - val unused = not(isAccountUsed(user)) + val unused = not(isAccountUsed(otherUser)) val Token(tokenName, tokenValue) = CSRF.getToken.get eventService .log( UserShowed, "Visualise la vue de modification l'utilisateur ", - involvesUser = Some(user.id) + involvesUser = Some(otherUser.id) ) Future( Ok( views.html.editUser(request.currentUser, request.rights)( form, - user, + otherUser, groups, unused, tokenName = tokenName, @@ -387,13 +387,7 @@ case class UserController @Inject() ( ) ) ) - case _ => - eventService.log( - ViewUserUnauthorized, - s"Accès non autorisé pour voir $userId", - involvesUser = userId.some - ) - Future(Unauthorized("Vous n'avez pas le droit de faire ça")) + } } } } diff --git a/app/models/Authorization.scala b/app/models/Authorization.scala index 9a75b9fb2..8fccdcff8 100644 --- a/app/models/Authorization.scala +++ b/app/models/Authorization.scala @@ -14,6 +14,7 @@ object Authorization { Set[Option[UserRight]]( HasUserId(user.id).some, IsInGroups(user.groupIds.toSet).some, + IsInAreas(user.areas.toSet).some, if (user.helper && not(user.disabled)) Helper.some else none, if (user.expert && not(user.disabled)) ExpertOfAreas(user.areas.toSet).some @@ -39,6 +40,7 @@ object Authorization { object UserRight { case class HasUserId(id: UUID) extends UserRight case class IsInGroups(groups: Set[UUID]) extends UserRight + case class IsInAreas(areas: Set[UUID]) extends UserRight case object Helper extends UserRight case class ExpertOfAreas(expertOfAreas: Set[UUID]) extends UserRight case class InstructorOfGroups(groupsManaged: Set[UUID]) extends UserRight @@ -146,6 +148,18 @@ object Authorization { isObserver ) + def isInArea(areaId: UUID): Check = + _.rights.exists { + case UserRight.IsInAreas(areas) if areas.contains(areaId) => true + case _ => false + } + + def isInOneOfAreas(thoseAreas: Set[UUID]): Check = + _.rights.exists { + case UserRight.IsInAreas(areas) if areas.intersect(thoseAreas).nonEmpty => true + case _ => false + } + def canObserveOrganisation(organisationId: Organisation.Id): Check = _.rights.exists { case UserRight.ObserverOfOrganisations(organisations) => @@ -216,6 +230,9 @@ object Authorization { def canSeeApplicationsAsAdmin: Check = atLeastOneIsAuthorized(isAdmin, isManager) + def canSeeOtherUserNonPrivateViews(otherUser: User): Check = + rights => isAdmin(rights) && !otherUser.admin && isInOneOfAreas(otherUser.areas.toSet)(rights) + def isApplicationCreator(application: Application): Check = _.rights.exists { case UserRight.HasUserId(id) => application.creatorUserId === id diff --git a/app/models/EventType.scala b/app/models/EventType.scala index ed8967c31..958d79443 100644 --- a/app/models/EventType.scala +++ b/app/models/EventType.scala @@ -153,6 +153,8 @@ object EventType { object WipeDataComplete extends Info object WipeDataError extends Error + object MasqueradeUnauthorized extends Warn + // Signups object SignupFormShowed extends Info object SignupFormValidationError extends Warn diff --git a/app/views/editUser.scala.html b/app/views/editUser.scala.html index 0dd579d61..308c90b2e 100644 --- a/app/views/editUser.scala.html +++ b/app/views/editUser.scala.html @@ -217,7 +217,22 @@
Outils

- Aperçu de l'écran de toutes les demandes de l'utilisateur
+ + @if(Authorization.canSeeOtherUserNonPrivateViews(uneditedUser)(currentUserRights)) { + + Aperçu de l’écran de toutes les demandes de l’utilisateur + +
+ + Vue "Mes Groupes" de l’utilisateur + +
+ + Vue "Stats" de l’utilisateur + +
+ } + Log d'événements de l'utilisateur

diff --git a/app/views/stats/page.scala.html b/app/views/stats/page.scala.html index e29efadfd..bff52b989 100644 --- a/app/views/stats/page.scala.html +++ b/app/views/stats/page.scala.html @@ -1,13 +1,13 @@ @import java.time.LocalDate @import java.util.UUID -@(currentUser: User, currentUserRights: Authorization.UserRights)(result: Html, groupsThatCanBeFilteredBy: List[UserGroup], selectedAreaIds: List[UUID], selectedOrganisationIds: List[Organisation.Id], selectedGroupIds: List[UUID], creationMinDate: LocalDate, creationMaxDate: LocalDate)(implicit webJarsUtil: org.webjars.play.WebJarsUtil, flash: Flash, request: RequestHeader, mainInfos: MainInfos) +@(currentUser: User, currentUserRights: Authorization.UserRights)(formUrl: Call, result: Html, groupsThatCanBeFilteredBy: List[UserGroup], selectedAreaIds: List[UUID], selectedOrganisationIds: List[Organisation.Id], selectedGroupIds: List[UUID], creationMinDate: LocalDate, creationMaxDate: LocalDate)(implicit webJarsUtil: org.webjars.play.WebJarsUtil, flash: Flash, request: RequestHeader, mainInfos: MainInfos) @main(currentUser, currentUserRights)(s"Stats") { @webJarsUtil.locate("Chart.bundle.min.js").script() }{
- @helper.form(routes.ApplicationController.stats, "method" -> "post") { + @helper.form(formUrl, "method" -> "post") { Département :