Reimplement authentication

This commit is contained in:
Thibault Duplessis 2012-05-26 14:04:22 +02:00
parent 97327a733e
commit 158dab0b5c
14 changed files with 130 additions and 108 deletions

View file

@ -1,17 +1,19 @@
package controllers
import lila._
import security.AuthConfigImpl
import views._
import jp.t2v.lab.play20.auth.LoginLogout
import play.api.data._
import play.api.data.Forms._
import play.api.templates._
import views._
import play.api.mvc._
import play.api.mvc.Results._
import ornicar.scalalib.OrnicarRandom
object Auth extends LilaController with LoginLogout with AuthConfigImpl {
object Auth extends LilaController {
def userRepo = env.user.userRepo
def store = env.securityStore
def login = Action { implicit req
Ok(html.auth.login(loginForm))
@ -30,4 +32,15 @@ object Auth extends LilaController with LoginLogout with AuthConfigImpl {
)
)
}
def gotoLoginSucceeded[A](username: String)(implicit req: RequestHeader) = {
val sessionId = OrnicarRandom nextAsciiString 16
store.save(sessionId, username, req)
loginSucceeded(req).withSession("sessionId" -> sessionId)
}
def gotoLogoutSucceeded(implicit req: RequestHeader) = {
req.session.get("sessionId") foreach store.delete
logoutSucceeded(req).withNewSession
}
}

View file

@ -2,7 +2,7 @@ package controllers
import lila._
import user.{ User UserModel }
import security.{ AuthConfigImpl, Permission }
import security.{ AuthImpl, Permission }
import http.{ Context, BodyContext, HttpEnvironment }
import core.Global
@ -21,12 +21,9 @@ trait LilaController
with HttpEnvironment
with ContentTypes
with RequestGetter
with AuthConfigImpl
with Auth {
with AuthImpl {
lazy val env = Global.env
lazy val cache = env.mongodb.cache
implicit val current = env.app
override implicit def lang(implicit req: RequestHeader) =
@ -46,28 +43,28 @@ 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] =
def Auth(f: Context UserModel Result): Action[AnyContent] =
Auth(BodyParsers.parse.anyContent)(f)
def Auth[A](p: BodyParser[A])(f: Context User Result): Action[A] =
def Auth[A](p: BodyParser[A])(f: Context UserModel 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] =
def AuthBody(f: BodyContext UserModel Result): Action[AnyContent] =
AuthBody(BodyParsers.parse.anyContent)(f)
def AuthBody[A](p: BodyParser[A])(f: BodyContext User Result): Action[A] =
def AuthBody[A](p: BodyParser[A])(f: BodyContext UserModel 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] =
def Secure(perm: Permission)(f: Context UserModel Result): Action[AnyContent] =
Secure(BodyParsers.parse.anyContent)(perm)(f)
def Secure[A](p: BodyParser[A])(perm: Permission)(f: Context User Result): Action[A] =
def Secure[A](p: BodyParser[A])(perm: Permission)(f: Context UserModel Result): Action[A] =
Auth(p) { ctx
me
ctx.isGranted(perm).fold(f(ctx)(me), authorizationFailed(ctx.req))
@ -168,24 +165,7 @@ trait LilaController
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)),
data success(data)
)
}
protected def reqToCtx(req: Request[_]) = Context(req, restoreUser(req))
protected def reqToCtx(req: RequestHeader) = Context(req, restoreUser(req))
private def restoreUser[A](req: RequestHeader): Option[UserModel] = for {
sessionId req.session.get("sessionId")
userId cache.getAs[Id](sessionId + ":sessionId")(idManifest)
user resolveUser(userId)
} yield {
cache.set(sessionId + ":sessionId", userId, sessionTimeoutInSeconds)
cache.set(userId.toString + ":userId", sessionId, sessionTimeoutInSeconds)
user
}
}

View file

@ -99,5 +99,6 @@ object User extends LilaController {
def export(username: String) = TODO
val onlineUsers: IO[List[User]] = userRepo byUsernames env.user.usernameMemo.keys
private val onlineUsers: IO[List[UserModel]] =
userRepo byUsernames env.user.usernameMemo.keys
}

View file

@ -95,6 +95,9 @@ final class CoreEnv private (application: Application, val settings: Settings) {
messageRepo = lobby.messageRepo,
entryRepo = timeline.entryRepo)
lazy val securityStore = new security.Store(
collection = mongodb(MongoCollectionSecurity))
lazy val gameFinishCommand = new command.GameFinish(
gameRepo = game.gameRepo,
finisher = round.finisher)

View file

@ -59,6 +59,7 @@ final class Settings(config: Config) {
val MongoCollectionRoom = getString("mongo.collection.room")
val MongoCollectionConfig = getString("mongo.collection.config")
val MongoCollectionCache = getString("mongo.collection.cache")
val MongoCollectionSecurity = getString("mongo.collection.security")
val ActorReporting = "reporting"
val ActorSiteHub = "site_hub"

View file

@ -15,15 +15,15 @@ case class Member(
def ownsHook(hook: Hook) = Some(hook.ownerId) == hookOwnerId
}
case class WithHooks(op: Iterable[String] => IO[Unit])
case class WithHooks(op: Iterable[String] IO[Unit])
case class AddHook(hook: Hook)
case class RemoveHook(hook: Hook)
case class BiteHook(hook: Hook, game: DbGame)
case class AddEntry(entry: Entry)
case class Join(
uid: String,
username: Option[String],
version: Int,
hookOwnerId: Option[String])
uid: String,
username: Option[String],
version: Int,
hookOwnerId: Option[String])
case class Talk(txt: String, u: String)
case class Connected(channel: Channel)

View file

@ -8,10 +8,8 @@ final class Cache(collection: MongoCollection) {
private val field = "v"
def set(key: String, value: Any): Unit = value match {
case null | None remove(key)
case Some(v) set(key, v)
case v collection += DBObject("_id" -> key, field -> v)
def set(key: String, value: Any): Unit = {
collection += DBObject("_id" -> key, field -> value)
}
def get(key: String): Option[Any] = for {

View file

@ -1,64 +0,0 @@
package lila
package security
import controllers.routes
import play.api._
import play.api.mvc._
import play.api.mvc.Results._
import play.api.mvc.{Request, PlainResult, Controller}
import play.api.data._
import play.api.data.Forms._
import jp.t2v.lab.play20.auth._
import core.CoreEnv
trait AuthConfigImpl extends AuthConfig {
val loginForm = Form(mapping(
"username" -> text,
"password" -> text
)(authenticateUser)(_.map(u (u.username, "")))
.verifying("Invalid username or password", result result.isDefined)
)
def env: CoreEnv
type Id = String
type User = user.User
type Authority = Permission
val idManifest: ClassManifest[Id] = classManifest[Id]
val sessionTimeoutInSeconds: Int = 3600 * 24 * 30
def resolveUser(id: Id): Option[User] =
(env.user.userRepo byUsername id).unsafePerformIO
def logoutSucceeded[A](req: Request[A]): PlainResult =
Redirect(routes.Lobby.home)
def authenticationFailed(req: RequestHeader): PlainResult =
Redirect(routes.Lobby.home).withSession("access_uri" -> req.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(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
def authenticateUser(username: String, password: String): Option[User] =
env.user.userRepo.authenticate(username, password).unsafePerformIO
}

View file

@ -0,0 +1,49 @@
package lila
package security
import controllers.routes
import user.User
import play.api._
import play.api.mvc._
import play.api.mvc.Results._
import play.api.mvc.{ Request, PlainResult, Controller }
import play.api.data._
import play.api.data.Forms._
import core.CoreEnv
trait AuthImpl {
val loginForm = Form(mapping(
"username" -> text,
"password" -> text
)(authenticateUser)(_.map(u (u.username, "")))
.verifying("Invalid username or password", result result.isDefined)
)
def env: CoreEnv
def logoutSucceeded(req: RequestHeader): PlainResult =
Redirect(routes.Lobby.home)
def authenticationFailed(req: RequestHeader): PlainResult =
Redirect(routes.Lobby.home).withSession("access_uri" -> req.uri)
def loginSucceeded(req: RequestHeader): PlainResult = {
val uri = req.session.get("access_uri").getOrElse(routes.Lobby.home.url)
req.session - "access_uri"
Redirect(uri)
}
def authorizationFailed(req: RequestHeader): PlainResult =
Forbidden("no permission")
def authenticateUser(username: String, password: String): Option[User] =
env.user.userRepo.authenticate(username, password).unsafePerformIO
def restoreUser(req: RequestHeader): Option[User] = for {
sessionId req.session.get("sessionId")
username env.securityStore.getUsername(sessionId)
user (env.user.userRepo byUsername username).unsafePerformIO
} yield user
}

40
app/security/Store.scala Normal file
View file

@ -0,0 +1,40 @@
package lila
package security
import user.User
import play.api.mvc.RequestHeader
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import java.util.Date
final class Store(collection: MongoCollection) {
def save(sessionId: String, username: String, req: RequestHeader) {
collection += DBObject(
"_id" -> sessionId,
"user" -> normalize(username),
"ip" -> ip(req),
"ua" -> ua(req),
"date" -> new Date)
}
def getUsername(sessionId: String): Option[String] = for {
obj collection.findOneByID(
sessionId,
DBObject("user" -> true)
)
v obj.getAs[String]("user")
} yield v
def delete(sessionId: String) {
collection.remove(DBObject("_id" -> sessionId))
}
// nginx: proxy_set_header X-Real-IP $remote_addr;
private def ip(req: RequestHeader) = req.headers.get("X-Real-IP") | "0.0.0.0"
private def ua(req: RequestHeader) = req.headers.get("User-Agent") | "?"
private def normalize(username: String) = username.toLowerCase
}

View file

@ -12,6 +12,7 @@ mongo {
room = room
config = config
cache = cache
security = security
}
connectionsPerHost = 100
autoConnectRetry = true

1
nginx
View file

@ -8,6 +8,7 @@ server {
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://127.0.0.1:9000/;
}

View file

@ -25,7 +25,6 @@ trait Dependencies {
val scalaTime = "org.scala-tools.time" %% "time" % "0.5"
val slf4jNop = "org.slf4j" % "slf4j-nop" % "1.6.4"
val dispatch = "net.databinder" %% "dispatch-http" % "0.8.7"
val auth = "jp.t2v" %% "play20.auth" % "0.3-SNAPSHOT"
val paginator = "com.github.ornicar" %% "paginator-core" % "1.5"
val paginatorSalat = "com.github.ornicar" %% "paginator-salat-adapter" % "1.4"
}
@ -56,7 +55,6 @@ object ApplicationBuild extends Build with Resolvers with Dependencies {
apache,
scalaTime,
dispatch,
auth,
paginator,
paginatorSalat),
templatesImport ++= Seq(

1
todo
View file

@ -23,6 +23,7 @@ use POST instead of GET where it makes sense
next deploy:
upgrade jre (jdk?)
db.user.update({},{$unset:{lastLogin: true}}, false, true)
db.user.update({},{$unset:{isOnline: true}}, false, true)
db.user.update({},{$unset:{gameConfigs: true}}, false, true)
db.user.update({},{$unset:{email: true}}, false, true)