Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow marked closed accounts to login for /appeal only #591

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/controllers/Auth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ final class Auth(
) map authenticateCookie(sessionId)
} recoverWith authRecovery

private def authenticateAppealUser(u: UserModel, redirect: String => Result)(implicit
ctx: Context
): Fu[Result] =
api.appeal.saveAuthentication(u.id) flatMap { sessionId =>
negotiate(
html = redirect(routes.Appeal.home.url).fuccess,
api = _ => NotFound.fuccess
) map authenticateCookie(sessionId)
} recoverWith authRecovery

private def authenticateCookie(sessionId: String)(result: Result)(implicit req: RequestHeader) =
result.withCookies(
env.lilaCookie.withSession {
Expand Down Expand Up @@ -111,7 +121,9 @@ final class Auth(
case None => InternalServerError("Authentication error").fuccess
case Some(u) if u.disabled =>
negotiate(
html = redirectTo(routes.Account.reopen.url).fuccess,
html =
if (u.marks.dirty) authenticateAppealUser(u, redirectTo)
else redirectTo(routes.Account.reopen.url).fuccess,
api = _ => Unauthorized(jsonError("This account is closed.")).fuccess
)
case Some(u) =>
Expand Down
67 changes: 34 additions & 33 deletions app/controllers/LilaController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import lila.common.{ ApiVersion, HTTPRequest, Nonce }
import lila.i18n.I18nLangPicker
import lila.notify.Notification.Notifies
import lila.oauth.{ OAuthScope, OAuthServer }
import lila.security.{ FingerPrintedUser, Granter, Permission }
import lila.security.{ AppealUser, FingerPrintedUser, Granter, Permission }
import lila.user.{ User => UserModel, UserContext }

abstract private[controllers] class LilaController(val env: Env)
Expand Down Expand Up @@ -353,11 +353,6 @@ abstract private[controllers] class LilaController(val env: Env)
_.fold(notFoundJson())(a => fuccess(Ok(Json toJson a) as JSON))
}

protected def JsOk(fua: Fu[String], headers: (String, String)*) =
fua map { a =>
Ok(a) as JAVASCRIPT withHeaders (headers: _*)
}

protected def FormResult[A](form: Form[A])(op: A => Fu[Result])(implicit req: Request[_]): Fu[Result] =
form
.bindFromRequest()
Expand Down Expand Up @@ -454,7 +449,9 @@ abstract private[controllers] class LilaController(val env: Env)
protected def authenticationFailed(implicit ctx: Context): Fu[Result] =
negotiate(
html = fuccess {
Redirect(routes.Auth.signup) withCookies env.lilaCookie
Redirect(
if (HTTPRequest.isAppeal(ctx.req)) routes.Auth.login else routes.Auth.signup
) withCookies env.lilaCookie
.session(env.security.api.AccessUri, ctx.req.uri)
},
api = _ =>
Expand Down Expand Up @@ -512,33 +509,34 @@ abstract private[controllers] class LilaController(val env: Env)
ctx.me.fold(fuccess(PageData.anon(ctx.req, nonce, blindMode(ctx)))) { me =>
env.pref.api.getPref(me, ctx.req) zip
(if (isGranted(_.Teacher, me)) fuccess(true) else env.clas.api.student.isStudent(me.id)) zip {
if (isPage) {
env.user.lightUserApi preloadUser me
env.team.api.nbRequests(me.id) zip
env.challenge.api.countInFor.get(me.id) zip
env.notifyM.api.unreadCount(Notifies(me.id)).dmap(_.value) zip
env.mod.inquiryApi.forMod(me)
} else
fuccess {
(((0, 0), 0), none)
}
} map {
if (isPage) {
env.user.lightUserApi preloadUser me
val enabledId = me.enabled option me.id
enabledId.??(env.team.api.nbRequests) zip
enabledId.??(env.challenge.api.countInFor.get) zip
enabledId.??(id => env.notifyM.api.unreadCount(Notifies(id)).dmap(_.value)) zip
env.mod.inquiryApi.forMod(me)
} else
fuccess {
(((0, 0), 0), none)
}
} map {
case (
(pref, hasClas),
(((teamNbRequests, nbChallenges), nbNotifications), inquiry)
) =>
PageData(
teamNbRequests,
nbChallenges,
nbNotifications,
pref,
blindMode = blindMode(ctx),
hasFingerprint = hasFingerPrint,
hasClas = hasClas,
inquiry = inquiry,
nonce = nonce
)
}
PageData(
teamNbRequests,
nbChallenges,
nbNotifications,
pref,
blindMode = blindMode(ctx),
hasFingerprint = hasFingerPrint,
hasClas = hasClas,
inquiry = inquiry,
nonce = nonce
)
}
}
}

Expand All @@ -551,13 +549,16 @@ abstract private[controllers] class LilaController(val env: Env)
type RestoredUser = (Option[FingerPrintedUser], Option[UserModel])
private def restoreUser(req: RequestHeader): Fu[RestoredUser] =
env.security.api restoreUser req dmap {
case Some(d) if !env.isProd =>
case Some(Left(AppealUser(user))) if HTTPRequest.isAppeal(req) =>
FingerPrintedUser(user, true).some
case Some(Right(d)) if !env.isProd =>
d.copy(user =
d.user
.addRole(lila.security.Permission.Beta.dbKey)
.addRole(lila.security.Permission.Prismic.dbKey)
).some
case d => d
case Some(Right(d)) => d.some
case _ => none
} flatMap {
case None => fuccess(None -> None)
case Some(d) =>
Expand Down Expand Up @@ -664,4 +665,4 @@ abstract private[controllers] class LilaController(val env: Env)

protected val notationContentType = "text/plain" // I guess, there is nothing better for shogi
protected val ndJsonContentType = "application/x-ndjson"
}
}
17 changes: 11 additions & 6 deletions app/views/base/layout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ object layout {
wrapClass -> wrapClass.nonEmpty
)
)(body),
ctx.isAuth option div(
ctx.me.exists(_.enabled) option div(
id := "friend_box",
dataPreload := safeJsonValue(Json.obj("i18n" -> i18nJsObject(i18nKeys)))
)(
Expand Down Expand Up @@ -348,16 +348,21 @@ object layout {
)
),
ctx.blind option h2("Navigation"),
topnav()
!ctx.isAppealUser option topnav()
),
div(cls := "site-buttons")(
if (ctx.req.path == "/") switchLanguage else "",
clinput,
!ctx.isAppealUser option clinput,
reports,
teamRequests,
ctx.me map { me =>
frag(allNotifications, dasher(me))
} getOrElse { !ctx.pageData.error option anonDasher(playing) }
if (ctx.isAppealUser)
postForm(action := routes.Auth.logout)(
submitButton(cls := "button button-red link")(trans.logOut())
)
else
ctx.me map { me =>
frag(allNotifications, dasher(me))
} getOrElse { !ctx.pageData.error option anonDasher(playing) }
)
)
}
Expand Down
2 changes: 2 additions & 0 deletions modules/common/src/main/HTTPRequest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ object HTTPRequest {
}
}

def isAppeal(req: RequestHeader) = req.path.startsWith("/appeal")

def clientName(req: RequestHeader) =
if (isXhr(req)) "xhr"
else if (isCrawler(req)) "crawler"
Expand Down
10 changes: 10 additions & 0 deletions modules/memo/src/main/CacheApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ final class CacheApi(
cache
}

def notLoadingSync[K, V](initialCapacity: Int, name: String)(
build: Builder => Cache[K, V]
): Cache[K, V] = {
val cache = build {
scaffeine.recordStats().initialCapacity(actualCapacity(initialCapacity))
}
monitor(name, cache)
cache
}

def monitor(name: String, cache: AsyncCache[_, _]): Unit =
monitor(name, cache.underlying.synchronous)

Expand Down
4 changes: 2 additions & 2 deletions modules/security/src/main/Granter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import lila.user.User
object Granter {

def apply(permission: Permission)(user: User): Boolean =
apply(permission, user.roles)
user.enabled && apply(permission, user.roles)

def apply(f: Permission.Selector)(user: User): Boolean =
apply(f(Permission), user.roles)
user.enabled && apply(f(Permission), user.roles)

def apply(permission: Permission, roles: Seq[String]): Boolean =
Permission(roles).exists(_ is permission)
Expand Down
4 changes: 3 additions & 1 deletion modules/security/src/main/MagicLink.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ ${Mailgun.txt.serviceNote}
}

def confirm(token: String): Fu[Option[User]] =
tokener read token flatMap { _ ?? userRepo.byId }
tokener read token flatMap { _ ?? userRepo.byId } map {
_.filter(_.canFullyLogin)
}

private val tokener = LoginToken.makeTokener(tokenerSecret, 10 minutes)
}
Expand Down
4 changes: 3 additions & 1 deletion modules/security/src/main/PasswordReset.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ ${Mailgun.txt.serviceNote}
}

def confirm(token: String): Fu[Option[User]] =
tokener read token flatMap { _ ?? userRepo.byId }
tokener read token flatMap { _ ?? userRepo.byId } map {
_.filter(_.canFullyLogin)
}

private val tokener = new StringToken[User.ID](
secret = tokenerSecret,
Expand Down
37 changes: 32 additions & 5 deletions modules/security/src/main/SecurityApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class SecurityApi(
userRepo: UserRepo,
store: Store,
firewall: Firewall,
cacheApi: lila.memo.CacheApi,
geoIP: GeoIP,
authenticator: lila.user.Authenticator,
emailValidator: EmailAddressValidator,
Expand Down Expand Up @@ -103,12 +104,16 @@ final class SecurityApi(
store.save(s"SIG-$sessionId", userId, req, apiVersion, up = false, fp = fp)
}

def restoreUser(req: RequestHeader): Fu[Option[FingerPrintedUser]] =
def restoreUser(req: RequestHeader): Fu[Option[Either[AppealUser, FingerPrintedUser]]] =
firewall.accepts(req) ?? reqSessionId(req) ?? { sessionId =>
store.authInfo(sessionId) flatMap {
_ ?? { d =>
userRepo byId d.user dmap { _ map { FingerPrintedUser(_, d.hasFp) } }
}
appeal.authenticate(sessionId) match {
case Some(userId) => userRepo byId userId map2 { u => Left(AppealUser(u)) }
case None =>
store.authInfo(sessionId) flatMap {
_ ?? { d =>
userRepo byId d.user dmap { _ map { u => Right(FingerPrintedUser(u, d.hasFp)) } }
}
}
}
}

Expand Down Expand Up @@ -187,6 +192,28 @@ final class SecurityApi(
),
ReadPreference.secondaryPreferred
)

// special temporary auth for marked closed accounts so they can use appeal endpoints
object appeal {

private type SessionId = String

private val prefix = "appeal:"

private val store = cacheApi.notLoadingSync[SessionId, User.ID](256, "security.session.appeal")(
_.expireAfterAccess(7.days).build()
)

def authenticate(sessionId: SessionId): Option[User.ID] =
sessionId.startsWith(prefix) ?? store.getIfPresent(sessionId)

def saveAuthentication(userId: User.ID)(implicit req: RequestHeader): Fu[SessionId] = {
val sessionId = s"$prefix${Random secureString 22}"
store.put(sessionId, userId)
logger.info(s"Appeal login by $userId")
fuccess(sessionId)
}
}
}

object SecurityApi {
Expand Down
2 changes: 2 additions & 0 deletions modules/security/src/main/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ case class AuthInfo(user: User.ID, hasFp: Boolean)

case class FingerPrintedUser(user: User, hasFingerPrint: Boolean)

case class AppealUser(user: User)

case class UserSession(
_id: String,
ip: IpAddress,
Expand Down
2 changes: 1 addition & 1 deletion modules/user/src/main/Authenticator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ final class Authenticator(
private def loginCandidate(select: Bdoc): Fu[Option[User.LoginCandidate]] =
userRepo.coll.one[AuthData](select, authProjection)(AuthDataBSONHandler) zip userRepo.coll
.one[User](select) map {
case (Some(authData), Some(user)) if user.enabled || !user.lameOrTroll =>
case (Some(authData), Some(user)) =>
User.LoginCandidate(user, authWithBenefits(authData)).some
case _ => none
}
Expand Down
4 changes: 3 additions & 1 deletion modules/user/src/main/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ case class User(
override def hashCode: Int = id.hashCode

override def toString =
s"User $username(${perfs.bestRating}) games:${count.game}${marks.troll ?? " troll"}${marks.engine ?? " engine"}"
s"User $username(${perfs.bestRating}) games:${count.game}${marks.troll ?? " troll"}${marks.engine ?? " engine"}${!enabled ?? " closed"}"

def light = LightUser(id = id, name = username, title = title.map(_.value), isPatron = isPatron)

Expand Down Expand Up @@ -79,6 +79,8 @@ case class User(
def lameOrAlt = lame || marks.alt
def lameOrTrollOrAlt = lameOrTroll || marks.alt

def canFullyLogin = enabled || !lameOrTrollOrAlt

def withMarks(f: UserMarks => UserMarks) = copy(marks = f(marks))

def lightPerf(key: String) =
Expand Down
1 change: 1 addition & 0 deletions modules/user/src/main/UserContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ trait UserContextWrapper extends UserContext {
val impersonatedBy = userContext.impersonatedBy
def isBot = me.exists(_.isBot)
def noBot = !isBot
def isAppealUser = me.exists(!_.enabled)
}

object UserContext {
Expand Down