From 33fde6d2932d05850c2a022b38bc505e39975065 Mon Sep 17 00:00:00 2001 From: Niklas Fiekas Date: Sun, 6 May 2018 12:03:28 +0200 Subject: [PATCH] allow mods to disable 2fa --- app/controllers/Account.scala | 17 ++++++++--------- app/controllers/Mod.scala | 4 ++++ app/views/user/mod.scala.html | 5 +++++ conf/routes | 1 + modules/mod/src/main/ModApi.scala | 4 ++++ modules/mod/src/main/Modlog.scala | 2 ++ modules/mod/src/main/ModlogApi.scala | 4 ++++ modules/security/src/main/DataForm.scala | 13 ++++++------- modules/security/src/main/Permission.scala | 5 +++-- modules/user/src/main/Authenticator.scala | 18 ------------------ modules/user/src/main/UserRepo.scala | 8 ++++++++ 11 files changed, 45 insertions(+), 36 deletions(-) diff --git a/app/controllers/Account.scala b/app/controllers/Account.scala index a165f618d2c2..842725b46fab 100644 --- a/app/controllers/Account.scala +++ b/app/controllers/Account.scala @@ -155,14 +155,14 @@ object Account extends LilaController { } def twoFactor = Auth { implicit ctx => me => - Env.user.authenticator.hasTotp(me.id) flatMap { - case false => Env.security.forms.setupTwoFactor(me) map { form => - html.account.setupTwoFactor(me, form) - } - case true => Env.security.forms.disableTwoFactor(me) map { form => + if (me.totpSecret.isDefined) + Env.security.forms.disableTwoFactor(me) map { form => html.account.disableTwoFactor(me, form) } - } + else + Env.security.forms.setupTwoFactor(me) map { form => + html.account.setupTwoFactor(me, form) + } } def setupTwoFactor = AuthBody { implicit ctx => me => @@ -171,7 +171,7 @@ object Account extends LilaController { FormFuResult(form) { err => fuccess(html.account.setupTwoFactor(me, err)) } { data => - Env.user.authenticator.setTotpSecret(me.id, TotpSecret(data.secret)) inject + UserRepo.setupTwoFactor(me.id, TotpSecret(data.secret)) inject Redirect(routes.Account.twoFactor) } } @@ -183,8 +183,7 @@ object Account extends LilaController { FormFuResult(form) { err => fuccess(html.account.disableTwoFactor(me, err)) } { _ => - Env.user.authenticator.unsetTotpSecret(me.id) inject - Redirect(routes.Account.twoFactor) + UserRepo.disableTwoFactor(me.id) inject Redirect(routes.Account.twoFactor) } } } diff --git a/app/controllers/Mod.scala b/app/controllers/Mod.scala index 75a0e6e52b4a..a1e82426675f 100644 --- a/app/controllers/Mod.scala +++ b/app/controllers/Mod.scala @@ -84,6 +84,10 @@ object Mod extends LilaController { } } + def disableTwoFactor(username: String) = Secure(_.DisableTwoFactor) { implicit ctx => me => + modApi.disableTwoFactor(me.id, username) >> User.modZoneOrRedirect(username, me) + } + def closeAccount(username: String) = Secure(_.CloseAccount) { implicit ctx => me => modApi.closeAccount(me.id, username).flatMap { _.?? { user => diff --git a/app/views/user/mod.scala.html b/app/views/user/mod.scala.html index e4d62777241e..5adeaf67f87a 100644 --- a/app/views/user/mod.scala.html +++ b/app/views/user/mod.scala.html @@ -44,6 +44,11 @@ } + @if(u.totpSecret.isDefined && isGranted(_.DisableTwoFactor)) { +
+ +
+ } @if(u.enabled) { @if(isGranted(_.CloseAccount)) {
diff --git a/conf/routes b/conf/routes index a939654e8080..2a275e194495 100644 --- a/conf/routes +++ b/conf/routes @@ -361,6 +361,7 @@ POST /mod/:username/troll/:v controllers.Mod.troll(username: String, v POST /mod/:username/ban/:v controllers.Mod.ban(username: String, v: Boolean) POST /mod/:username/delete-pms-and-chats controllers.Mod.deletePmsAndChats(username: String) POST /mod/:username/warn controllers.Mod.warn(username: String, subject: String) +POST /mod/:username/disable-2fa controllers.Mod.disableTwoFactor(username: String) POST /mod/:username/close controllers.Mod.closeAccount(username: String) POST /mod/:username/reopen controllers.Mod.reopenAccount(username: String) POST /mod/:username/title controllers.Mod.setTitle(username: String) diff --git a/modules/mod/src/main/ModApi.scala b/modules/mod/src/main/ModApi.scala index 9b6029542ee5..e2cc04e9de73 100644 --- a/modules/mod/src/main/ModApi.scala +++ b/modules/mod/src/main/ModApi.scala @@ -97,6 +97,10 @@ final class ModApi( _ <- ipBan ?? setBan(mod, sus, true) } yield logApi.garbageCollect(mod, sus) + def disableTwoFactor(mod: String, username: String): Funit = withUser(username) { user => + (UserRepo disableTwoFactor user.id) >> logApi.disableTwoFactor(mod, user.id) + } + def closeAccount(mod: String, username: String): Fu[Option[User]] = withUser(username) { user => user.enabled ?? { logApi.closeAccount(mod, user.id) inject user.some diff --git a/modules/mod/src/main/Modlog.scala b/modules/mod/src/main/Modlog.scala index d837089d44e2..c57a893eb140 100644 --- a/modules/mod/src/main/Modlog.scala +++ b/modules/mod/src/main/Modlog.scala @@ -21,6 +21,7 @@ case class Modlog( case Modlog.ban => "ban user" case Modlog.ipban => "ban IPs" case Modlog.ipunban => "unban IPs" + case Modlog.disableTwoFactor => "disable 2fa" case Modlog.closeAccount => "close account" case Modlog.selfCloseAccount => "self close account" case Modlog.reopenAccount => "reopen account" @@ -82,6 +83,7 @@ object Modlog { val permissions = "permissions" val ban = "ban" val ipban = "ipban" + val disableTwoFactor = "disableTwoFactor" val closeAccount = "closeAccount" val selfCloseAccount = "selfCloseAccount" val reopenAccount = "reopenAccount" diff --git a/modules/mod/src/main/ModlogApi.scala b/modules/mod/src/main/ModlogApi.scala index 4ffb1ae9be57..05609ca025b7 100644 --- a/modules/mod/src/main/ModlogApi.scala +++ b/modules/mod/src/main/ModlogApi.scala @@ -36,6 +36,10 @@ final class ModlogApi(coll: Coll) { Modlog.make(mod, sus, if (sus.user.ipBan) Modlog.ipban else Modlog.ipunban) } + def disableTwoFactor(mod: String, user: String) = add { + Modlog(mod, user.some, Modlog.disableTwoFactor) + } + def closeAccount(mod: String, user: String) = add { Modlog(mod, user.some, Modlog.closeAccount) } diff --git a/modules/security/src/main/DataForm.scala b/modules/security/src/main/DataForm.scala index 99cfa53116ad..c02886d4f5f9 100644 --- a/modules/security/src/main/DataForm.scala +++ b/modules/security/src/main/DataForm.scala @@ -116,13 +116,12 @@ final class DataForm( )) } - def disableTwoFactor(u: User) = for { - candidate <- authenticator loginCandidate u - totpSecret <- authenticator totpSecret u.id - } yield Form(tuple( - "passwd" -> nonEmptyText.verifying("incorrectPassword", p => candidate.check(ClearPassword(p))), - "token" -> nonEmptyText.verifying("invalidAuthenticationToken", t => totpSecret.map(_.verify(t)).getOrElse(false)) - )) + def disableTwoFactor(u: User) = authenticator loginCandidate u map { candidate => + Form(tuple( + "passwd" -> nonEmptyText.verifying("incorrectPassword", p => candidate.check(ClearPassword(p))), + "token" -> nonEmptyText.verifying("invalidAuthenticationToken", t => u.totpSecret.map(_.verify(t)).getOrElse(false)) + )) + } def fixEmail(old: EmailAddress) = Form( single("email" -> acceptableUniqueEmail(none).verifying(emailValidator differentConstraint old.some)) diff --git a/modules/security/src/main/Permission.scala b/modules/security/src/main/Permission.scala index 2fbc6cd0d30c..b523839d3cf6 100644 --- a/modules/security/src/main/Permission.scala +++ b/modules/security/src/main/Permission.scala @@ -21,6 +21,7 @@ object Permission { case object MarkEngine extends Permission("ROLE_ADJUST_CHEATER", List(UserSpy)) case object MarkBooster extends Permission("ROLE_ADJUST_BOOSTER", List(UserSpy)) case object IpBan extends Permission("ROLE_IP_BAN", List(UserSpy)) + case object DisableTwoFactor extends Permission("ROLE_DISABLE_2FA") case object CloseAccount extends Permission("ROLE_CLOSE_ACCOUNT", List(UserSpy)) case object ReopenAccount extends Permission("ROLE_REOPEN_ACCOUNT", List(UserSpy)) case object SetTitle extends Permission("ROLE_SET_TITLE", List(UserSpy)) @@ -64,7 +65,7 @@ object Permission { ChatTimeout, MarkTroll, SetTitle, SetEmail, ModerateQa, StreamConfig, MessageAnyone, CloseTeam, TerminateTournament, ManageTournament, ManageEvent, PracticeConfig, RemoveRanking, ReportBan, Beta, DisapproveCoachReview, - Relay, Streamers + Relay, Streamers, DisableTwoFactor )) case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN", List( @@ -76,7 +77,7 @@ object Permission { UserSpy, MarkEngine, MarkBooster, IpBan, ModerateQa, StreamConfig, PracticeConfig, Beta, MessageAnyone, UserSearch, CloseTeam, TerminateTournament, ManageTournament, ManageEvent, PublicMod, Developer, Coach, ModNote, RemoveRanking, ReportBan, - Relay, Cli, Settings, Streamers + Relay, Cli, Settings, Streamers, DisableTwoFactor ) lazy private val all: List[Permission] = SuperAdmin :: allButSuperAdmin diff --git a/modules/user/src/main/Authenticator.scala b/modules/user/src/main/Authenticator.scala index fe28f7acc640..6c84b500d4a6 100644 --- a/modules/user/src/main/Authenticator.scala +++ b/modules/user/src/main/Authenticator.scala @@ -44,24 +44,6 @@ final class Authenticator( $set(F.bpass -> passEnc(p).bytes) ++ $unset(F.salt, F.sha512) ).void - def setTotpSecret(id: User.ID, totp: TotpSecret): Funit = - userRepo.coll.update( - $id(id) ++ (F.totpSecret $exists false), // never overwrite existing secret - $set(F.totpSecret -> totp.secret) - ).void - - def unsetTotpSecret(id: User.ID): Funit = - userRepo.coll.update( - $id(id), - $unset(F.totpSecret) - ).void - - def totpSecret(id: User.ID): Fu[Option[TotpSecret]] = - userRepo.coll.primitiveOne[Array[Byte]]($id(id), F.totpSecret).map(_ map TotpSecret.apply) - - def hasTotp(id: User.ID): Fu[Boolean] = - userRepo.coll.exists($id(id) ++ $doc(F.totpSecret $exists true)) - private def authWithBenefits(auth: AuthData)(p: ClearPassword): Boolean = { val res = compare(auth, p) if (res && auth.salt.isDefined) diff --git a/modules/user/src/main/UserRepo.scala b/modules/user/src/main/UserRepo.scala index c5d71d8c2e07..0698535dce2f 100644 --- a/modules/user/src/main/UserRepo.scala +++ b/modules/user/src/main/UserRepo.scala @@ -297,6 +297,14 @@ object UserRepo { def setRoles(id: ID, roles: List[String]) = coll.updateField($id(id), "roles", roles) + def disableTwoFactor(id: ID) = coll.update($id(id), $unset(F.totpSecret)) + + def setupTwoFactor(id: ID, totp: TotpSecret): Funit = + coll.update( + $id(id) ++ (F.totpSecret $exists false), // never overwrite existing secret + $set(F.totpSecret -> totp.secret) + ).void + def enable(id: ID) = coll.updateField($id(id), F.enabled, true) def disable(user: User, keepEmail: Boolean) = coll.update(