User and security

This commit is contained in:
Thibault Duplessis 2012-05-25 00:01:47 +02:00
parent a1914fbe40
commit 58946f8177
27 changed files with 378 additions and 94 deletions

View file

@ -2,7 +2,7 @@ package controllers
import lila._
import user.{ User UserModel }
import security.{ AuthConfigImpl }
import security.{ AuthConfigImpl, Permission }
import http.{ Context, BodyContext, HttpEnvironment }
import core.Global
@ -46,6 +46,33 @@ trait LilaController
def OpenBody[A](p: BodyParser[A])(f: BodyContext Result): Action[A] =
Action(p)(req f(reqToCtx(req)))
def Auth(f: Context User Result): Action[AnyContent] =
Auth(BodyParsers.parse.anyContent)(f)
def Auth[A](p: BodyParser[A])(f: Context User Result): Action[A] =
Action(p)(req {
val ctx = reqToCtx(req)
ctx.me.fold(me f(ctx)(me), authenticationFailed(ctx.req))
})
def AuthBody(f: BodyContext User Result): Action[AnyContent] =
AuthBody(BodyParsers.parse.anyContent)(f)
def AuthBody[A](p: BodyParser[A])(f: BodyContext User Result): Action[A] =
Action(p)(req {
val ctx = reqToCtx(req)
ctx.me.fold(me f(ctx)(me), authenticationFailed(ctx.req))
})
def Secure(perm: Permission)(f: Context User Result): Action[AnyContent] =
Secure(BodyParsers.parse.anyContent)(perm)(f)
def Secure[A](p: BodyParser[A])(perm: Permission)(f: Context User Result): Action[A] =
Auth(p) { ctx
me
ctx.isGranted(perm).fold(f(ctx)(me), authorizationFailed(ctx.req))
}
def JsonOk(map: Map[String, Any]) = Ok(toJson(map)) as JSON
def JsonOk(list: List[String]) = Ok(Json generate list) as JSON
@ -133,6 +160,11 @@ trait LilaController
implicit def ctoInt: ContentTypeOf[Int] =
ContentTypeOf[Int](Some(ContentTypes.TEXT))
implicit def wOptionString: Writeable[Option[String]] =
Writeable[Option[String]](i Codec toUTF8 i.getOrElse(""))
implicit def ctoOptionString: ContentTypeOf[Option[String]] =
ContentTypeOf[Option[String]](Some(ContentTypes.TEXT))
implicit def richForm[A](form: Form[A]) = new {
def toValid: Valid[A] = form.fold(
form failure(nel("Invalid form", form.errors.map(_.toString).toList)),

View file

@ -2,6 +2,7 @@ package controllers
import lila._
import views._
import security.Permission
import play.api._
import play.api.mvc._
@ -13,23 +14,29 @@ object User extends LilaController {
def userRepo = env.user.userRepo
def paginator = env.user.paginator
def gamePaginator = env.game.paginator
def forms = user.DataForm
def show(username: String) = showMode(username, "all", 1)
def show(username: String) = showFilter(username, "all", 1)
def showMode(username: String, mode: String, page: Int) = Open { implicit ctx
IOptionIOk(userRepo byUsername username) { user
user.enabled.fold(
env.user.userInfo(user, ctx) map { info
html.user.show(
u = user,
info = info,
mode = mode,
games = gamePaginator.userAll(user, page))
def showFilter(username: String, filterName: String, page: Int) = Open { implicit ctx
IOptionIOk(userRepo byUsername username) { u
u.enabled.fold(
env.user.userInfo(u, ctx) map { info
val filters = user.GameFilterMenu(info, ctx.me)
val filter = filters(filterName)
val games = gamePaginator.userAll(u, page)
(u.some == ctx.me).fold(
html.user.home(u = u, info = info, games = games, filters = filters, filter = filter),
html.user.show(u = u, info = info, games = games, filters = filters, filter = filter)
)
},
io(html.user.disabled(user)))
io(html.user.disabled(u)))
}
}
def showTemplate(user: User, me: Option[User]) =
(user.some == me).fold(html.user.show, html.user.home)
def list(page: Int) = Open { implicit ctx
IOk(onlineUsers map { html.user.list(paginator elo page, _) })
}
@ -45,6 +52,43 @@ object User extends LilaController {
)
}
val getBio = Auth { ctx me Ok(me.bio) }
val setBio = AuthBody { ctx
me
implicit val req = ctx.body
IORedirect(forms.bio.bindFromRequest.fold(
f putStrLn(f.errors.toString) map { _ routes.User show me.username },
bio userRepo.setBio(me, bio) map { _ routes.User show me.username }
))
}
val close = Auth { implicit ctx
me
Ok(html.user.close(me))
}
val closeConfirm = Auth { ctx
me
IORedirect {
userRepo disable me map { _ routes.User show me.username }
}
}
def engine(username: String) = Secure(Permission.MarkEngine) { _
_
IORedirect {
userRepo toggleEngine username map { _ routes.User show username }
}
}
def mute(username: String) = Secure(Permission.MutePlayer) { _
_
IORedirect {
userRepo toggleMute username map { _ routes.User show username }
}
}
val signUp = TODO
val stats = TODO

View file

@ -153,19 +153,26 @@ class GameRepo(collection: MongoCollection)
def countDrawBy(user: User): IO[Int] = io {
count(
("status" $in List(Status.Draw.id, Status.Stalemate.id)) ++
("userIds" -> user.id.toString)
("status" $in List(Status.Draw.id, Status.Stalemate.id)) ++
("userIds" -> user.id.toString)
).toInt
}
def countLossBy(user: User): IO[Int] = io {
count(
("status" $in List(Status.Mate.id, Status.Resign.id, Status.Outoftime.id, Status.Timeout.id)) ++
("userIds" -> user.id.toString) ++
("winnerUserId" $ne user.id.toString)
("status" $in List(Status.Mate.id, Status.Resign.id, Status.Outoftime.id, Status.Timeout.id)) ++
("userIds" -> user.id.toString) ++
("winnerUserId" $ne user.id.toString)
).toInt
}
def countOpponents(user1: User, user2: User): IO[Int] = io {
count(opponentsQuery(user1, user2)).toInt
}
def opponentsQuery(user1: User, user2: User) =
"userIds" $all List(user1.id.toString, user2.id.toString)
def recentGames(limit: Int): IO[List[DbGame]] = io {
find(DBObject("status" -> Status.Started.id))
.sort(DBObject("updatedAt" -> -1))

View file

@ -2,6 +2,7 @@ package lila
package http
import user.User
import security.{ Permission, Granter }
import play.api.mvc.{ Request, RequestHeader }
@ -10,6 +11,9 @@ sealed abstract class Context(val req: RequestHeader, val me: Option[User]) {
def isAuth = me.isDefined
def canSeeChat = me.fold(m !m.isChatBan, false)
def isGranted(permission: Permission): Boolean =
me.fold(Granter(permission), false)
}
final class BodyContext(val body: Request[_], m: Option[User])

View file

@ -35,21 +35,27 @@ trait AuthConfigImpl extends AuthConfig {
def resolveUser(id: Id): Option[User] =
(env.user.userRepo byUsername id).unsafePerformIO
def logoutSucceeded[A](request: Request[A]): PlainResult =
def logoutSucceeded[A](req: Request[A]): PlainResult =
Redirect(routes.Lobby.home)
def authenticationFailed[A](request: Request[A]): PlainResult =
Redirect(routes.Lobby.home).withSession("access_uri" -> request.uri)
def authenticationFailed(req: RequestHeader): PlainResult =
Redirect(routes.Lobby.home).withSession("access_uri" -> req.uri)
def loginSucceeded[A](request: Request[A]): PlainResult = {
val uri = request.session.get("access_uri").getOrElse(routes.Lobby.home.url)
request.session - "access_uri"
def authenticationFailed[A](req: Request[A]): PlainResult =
authenticationFailed(req)
def loginSucceeded[A](req: Request[A]): PlainResult = {
val uri = req.session.get("access_uri").getOrElse(routes.Lobby.home.url)
req.session - "access_uri"
Redirect(uri)
}
def authorizationFailed[A](request: Request[A]): PlainResult =
def authorizationFailed(req: RequestHeader): PlainResult =
Forbidden("no permission")
def authorizationFailed[A](req: Request[A]): PlainResult =
authorizationFailed(req)
def authorize(user: User, authority: Authority): Boolean =
Permission(user.roles) contains authority

View file

@ -0,0 +1,10 @@
package lila
package security
import user.User
object Granter {
def apply(permission: Permission)(user: User): Boolean =
Permission(user.roles) exists (_ is permission)
}

View file

@ -1,14 +1,26 @@
package lila
package security
sealed abstract class Permission(val name: String)
sealed abstract class Permission(val name: String) {
val children: List[Permission] = Nil
final def is(p: Permission): Boolean =
this == p || (children exists (_ is p))
}
object Permission {
case object Anonymous extends Permission("ANON")
case object Admin extends Permission("ROLE_ADMIN")
case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN")
case object ViewTrialsInGame extends Permission("ROLE_VIEW_TRIALS_IN_GAME")
case object ViewBlurs extends Permission("ROLE_VIEW_BLURS")
case object MutePlayer extends Permission("ROLE_CHAT_BAN")
case object MarkEngine extends Permission("ROLE_ADJUST_CHEATER")
case object Admin extends Permission("ROLE_ADMIN") {
override val children = List(ViewBlurs, MutePlayer, MarkEngine)
}
case object SuperAdmin extends Permission("ROLE_SUPER_ADMIN") {
override val children = List(Admin)
}
val all: List[Permission] = List(SuperAdmin, Admin)
val allByName: Map[String, Permission] = all map { p

View file

@ -1,10 +1,14 @@
package lila
package security
import user.User
import http.Context
trait SecurityHelper {
def isGranted(permission: Permission)(implicit ctx: Context): Boolean =
ctx.me.fold(_ hasRole permission.name, false)
ctx.me.fold(Granter(permission), false)
def isGranted(permission: Permission, user: User): Boolean =
Granter(permission)(user)
}

12
app/user/DataForm.scala Normal file
View file

@ -0,0 +1,12 @@
package lila
package user
import play.api.data._
import play.api.data.Forms._
object DataForm {
val bio = Form(single(
"bio" -> text(maxLength = 400)
))
}

31
app/user/GameFilter.scala Normal file
View file

@ -0,0 +1,31 @@
package lila
package user
import http.Context
import scalaz.NonEmptyLists
sealed abstract class GameFilter(val name: String)
object GameFilter {
case object All extends GameFilter("all")
case object Me extends GameFilter("me")
}
case class GameFilterMenu(
info: UserInfo,
me: Option[User]) extends NonEmptyLists {
import GameFilter._
val all = nel(All, List(
(info.user.some != me) option Me
).flatten)
def list = all.list
def apply(name: String) = (list find (_.name == name)) | default
def default = all.head
}

View file

@ -27,9 +27,9 @@ case class User(
def setting(name: String): Option[Any] = settings get name
def hasRole(name: String) = roles contains name
def nonEmptyBio = bio filter ("" !=)
def hasGames = nbGames > 0
}
object User {

View file

@ -8,15 +8,15 @@ import http.Context
import scalaz.effects._
case class UserInfo(
user: User,
rank: Option[(Int, Int)],
nbWin: Int,
nbDraw: Int,
nbLoss: Int,
eloWithMe: Option[List[(String, Int)]],
eloChart: Option[EloChart],
winChart: Option[WinChart]) {
}
user: User,
rank: Option[(Int, Int)],
nbWin: Int,
nbDraw: Int,
nbLoss: Int,
nbWithMe: Option[Int],
eloWithMe: Option[List[(String, Int)]],
eloChart: Option[EloChart],
winChart: Option[WinChart])
object UserInfo {
@ -37,6 +37,10 @@ object UserInfo {
nbWin gameRepo countWinBy user
nbDraw gameRepo countDrawBy user
nbLoss gameRepo countLossBy user
nbWithMe ctx.me.filter(user!=).fold(
me gameRepo.countOpponents(user, me) map (_.some),
io(none)
)
eloChart eloChartBuilder(user)
winChart = (user.nbGames > 0) option {
new WinChart(nbWin, nbDraw, nbLoss)
@ -53,6 +57,7 @@ object UserInfo {
nbWin = nbWin,
nbDraw = nbDraw,
nbLoss = nbLoss,
nbWithMe = nbWithMe,
eloWithMe = eloWithMe,
eloChart = eloChart,
winChart = winChart)

View file

@ -94,6 +94,29 @@ class UserRepo(
.flatten
}
def toggleMute(username: String): IO[Unit] = updateIO(username) { user
$set("isChatBan" -> !user.isChatBan)
}
def toggleEngine(username: String): IO[Unit] = updateIO(username) { user
$set("engine" -> !user.engine)
}
def setBio(user: User, bio: String) = updateIO(user)($set("bio" -> bio))
def enable(user: User) = updateIO(user)($set("enabled" -> true))
def disable(user: User) = updateIO(user)($set("enabled" -> false))
def updateIO(username: String)(op: User DBObject): IO[Unit] = for {
userOption byUsername(username)
_ userOption.fold(user updateIO(user)(op(user)), io())
} yield ()
def updateIO(user: User)(obj: DBObject): IO[Unit] = io {
update(idSelector(user), obj)
}
private def idSelector(user: User) = DBObject("_id" -> user.id)
private def idSelector(id: ObjectId) = DBObject("_id" -> id)

View file

@ -70,7 +70,7 @@
@game.clock.map { c =>
@round.clock(c, pov.color, "bottom")
}
@if(isGranted(Permission.ViewTrialsInGame)) {
@if(isGranted(Permission.ViewBlurs)) {
@game.players.map { p =>
@if(game.playerBlurPercent(p.color) > 30) {
<br />@playerLink(p) @p.blurs/@game.playerMoves(p.color) blurs (@game.playerBlurPercent(p.color))

View file

@ -0,0 +1,24 @@
@(u: User)(implicit ctx: Context)
@title = @{ "%s - close account".format(u.username) }
@user.layout(
title = title,
robots = false) {
<div class="content_box small_box">
<div class="signup_box">
<h1 class="lichess_title">Close your account</h1>
<p class="explanation">
Are you sure you want to close your account? You will no more be able to login!
</p>
<form action="@routes.User.closeConfirm" method="POST">
<br /><br />
<a href="@routes.User.show(u.username)">
I changed my mind, don't close my account
</a>
<br /><br />
<input type="submit" class="submit" value="Close my account" />
</form>
</div>
</div>
}

View file

@ -0,0 +1,11 @@
@(info: lila.user.UserInfo, filter: lila.user.GameFilter)(implicit ctx: Context)
@filter match {
case lila.user.GameFilter.All => {
@info.user.nbGames @trans.gamesPlayed()
}
case lila.user.GameFilter.Me => {
@ctx.me.fold(me => "%d vs %s".format(info.nbWithMe | 0, me.username), "-")
}
}

View file

@ -0,0 +1,23 @@
@(u: User, info: lila.user.UserInfo, games: Paginator[DbGame], filters: lila.user.GameFilterMenu, filter: lila.user.GameFilter)(implicit ctx: Context)
@bio = {
<div class="editable" data-url="@routes.User.setBio">
<span class="user_bio" data-name="bio" data-type="textarea" data-provider-url="@routes.User.getBio">
@shorten(u.bio | "Click here to tell about yourself.", 400)
</span>
</div>
}
@actions = {
<br />
<a class="small" href="@routes.User.close">Close your account</a>
}
@user.profile(
u = u,
info = info,
games = games,
bio = bio,
actions = actions,
filters = filters,
filter = filter)

View file

@ -1,4 +1,6 @@
@(u: User, info: lila.user.UserInfo, title: String, bio: Html)(implicit ctx: Context)
@(u: User, info: lila.user.UserInfo, games: Paginator[DbGame], bio: Html, actions: Html, filters: lila.user.GameFilterMenu, filter: lila.user.GameFilter)(implicit ctx: Context)
@title = @{ "%s %s - page %d".format(u.username, filterTitle(info, filter), games.currentPage) }
@evenMoreJs = {
<script src="http://www.google.com/jsapi"></script>
@ -25,7 +27,7 @@ evenMoreJs = evenMoreJs) {
}
</div>
<div class="content_box_content clearfix">
@if(u.hasRole("ROLE_ADMIN")) {
@if(isGranted(Permission.Admin, u)) {
<div class="staff">STAFF</div>
}
@info.eloChart.map { eloChart =>
@ -41,10 +43,26 @@ evenMoreJs = evenMoreJs) {
@e._1.capitalize: <strong>@showNumber(e._2)</strong>
}
</div>
}
<div class="stats">
@info.winChart.map { winChart =>
<div class="win_stats" title="@trans.gamesPlayed(): @u.nbGames" data-columns="@winChart.columns" data-rows="@winChart.rows(trans)"></div>
}
@actions
</div>
}
}
</div>
@if(u.hasGames) {
<div class="content_box_inter clearfix">
@filters.list.map { f =>
<a @(filter == f).fold("class='active' ", "")href="@routes.User.showFilter(u.username, f.name)">
@filterTitle(info, f)
</a>
}
</div>
<div class="games infinitescroll all_games">
<div class="pager"><a href="@routes.User.showFilter(u.username, filter.name, games.nextPage | 1)">Next</a></div>
@game.widgets(games.currentPageResults, u.some)
</div>
}
</div>
}

View file

@ -1,6 +1,4 @@
@(u: User, info: lila.user.UserInfo, mode: String, games: Paginator[DbGame])(implicit ctx: Context)
@title = @{ "%s %s - page %d".format(u.username, mode, games.currentPage) }
@(u: User, info: lila.user.UserInfo, games: Paginator[DbGame], filters: lila.user.GameFilterMenu, filter: lila.user.GameFilter)(implicit ctx: Context)
@bio = {
@u.nonEmptyBio.map { bio =>
@ -8,8 +6,24 @@
}
}
@actions = {
@if(isGranted(Permission.MarkEngine)) {
<form method="post" action="@routes.User.engine(u.username)">
<input class="confirm" type="submit" value="Mark as engine" />
</form>
}
@if(isGranted(Permission.MutePlayer)) {
<form method="post" action="@routes.User.mute(u.username)">
<input class="confirm" type="submit" value="@u.isChatBan.fold("Unmute", "Mute")" />
</form>
}
}
@user.profile(
u = u,
info = info,
title = title,
bio = bio)
games = games,
bio = bio,
actions = actions,
filters = filters,
filter = filter)

View file

@ -1,8 +0,0 @@
package lila.cli
import scalaz.effects._
trait Command {
def apply(): IO[Unit]
}

View file

@ -3,9 +3,9 @@ package lila.cli
import lila.game.GameRepo
import scalaz.effects._
case class IndexDb(gameRepo: GameRepo) extends Command {
case class Games(gameRepo: GameRepo) {
def apply: IO[Unit] = for {
def index: IO[Unit] = for {
_ putStrLn("- Drop indexes")
_ gameRepo.dropIndexes
_ putStrLn("- Ensure indexes")

View file

@ -1,16 +0,0 @@
package lila.cli
import lila.core.CoreEnv
import scalaz.effects._
case class Info(env: CoreEnv) extends Command {
def apply: IO[Unit] = for {
nb <- nbGames
_ putStrLn("%d games in DB" format nb)
} yield ()
def nbGames = io {
env.game.gameRepo.count()
}
}

View file

@ -3,9 +3,9 @@ package lila.cli
import lila.core.CoreEnv
import scalaz.effects._
case class AverageElo(env: CoreEnv) extends Command {
case class Infos(env: CoreEnv) {
def apply: IO[Unit] = for {
def averageElo: IO[Unit] = for {
avg env.user.userRepo.averageElo
_ putStrLn("Average elo is %f" format avg)
} yield ()

View file

@ -16,24 +16,24 @@ object Main {
lazy val env = CoreEnv(app)
lazy val users = Users(env.user.userRepo)
lazy val games = Games(env.game.gameRepo)
lazy val infos = Infos(env)
def main(args: Array[String]): Unit = sys exit {
val command: Command = args.toList match {
case "info" :: Nil Info(env)
case "average-elo" :: Nil AverageElo(env)
case "index" :: Nil IndexDb(env.game.gameRepo)
val op: IO[Unit] = args.toList match {
case "average-elo" :: Nil infos.averageElo
case "game-index" :: Nil games.index
case "trans-js-dump" :: Nil TransJsDump(
path = new File(env.app.path.getCanonicalPath + "/public/trans"),
pool = env.i18n.pool,
keys = env.i18n.keys)
case "finish" :: Nil new Command {
def apply() = env.gameFinishCommand.apply()
}
case _ new Command {
def apply() = putStrLn("Usage: run command args")
}
keys = env.i18n.keys).apply
case "finish" :: Nil env.gameFinishCommand.apply
case "user-enable" :: username :: Nil users enable username
case "user-disable" :: username :: Nil users disable username
case _ putStrLn("Usage: run command args")
}
command.apply.unsafePerformIO
0
op.map(_ 0).unsafePerformIO
}
}

View file

@ -8,7 +8,7 @@ import scalaz.effects._
case class TransJsDump(
path: File,
pool: I18nPool,
keys: I18nKeys) extends Command {
keys: I18nKeys) {
val messages = List(
keys.unlimited,

View file

@ -0,0 +1,22 @@
package lila.cli
import lila.user.{ User, UserRepo }
import scalaz.effects._
case class Users(userRepo: UserRepo) {
def enable(username: String): IO[Unit] =
perform(username, "Enable", userRepo.enable)
def disable(username: String): IO[Unit] =
perform(username, "Disable", userRepo.disable)
def perform(username: String, action: String, op: User IO[Unit]) = for {
_ putStrLn(action + " " + username)
userOption userRepo byUsername username
_ userOption.fold(
u op(u) flatMap { _ putStrLn("Success") },
putStrLn("Not found")
)
} yield ()
}

View file

@ -61,13 +61,19 @@ GET /logout controllers.Auth.logout
# User
GET /@/:username/export controllers.User.export(username: String)
GET /@/:username/:mode controllers.User.showMode(username: String, mode: String, page: Int ?= 1)
GET /@/:username/:filterName controllers.User.showFilter(username: String, filterName: String, page: Int ?= 1)
GET /@/:username controllers.User.show(username: String)
POST /@/:username/engine controllers.User.engine(username: String)
POST /@/:username/mute controllers.User.mute(username: String)
GET /signup controllers.User.signUp
GET /people controllers.User.list(page: Int ?= 1)
GET /people/stats controllers.User.stats
GET /people/autocomplete controllers.User.autocomplete
GET /people/online controllers.User.online
GET /account/bio controllers.User.getBio
POST /account/bio controllers.User.setBio
GET /account/close controllers.User.close
POST /account/closeConfirm controllers.User.closeConfirm
# Wiki
GET /wiki controllers.Wiki.home