Reimplement authentication
This commit is contained in:
parent
97327a733e
commit
158dab0b5c
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -15,7 +15,7 @@ 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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
49
app/security/AuthImpl.scala
Normal file
49
app/security/AuthImpl.scala
Normal 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
40
app/security/Store.scala
Normal 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
|
||||
}
|
|
@ -12,6 +12,7 @@ mongo {
|
|||
room = room
|
||||
config = config
|
||||
cache = cache
|
||||
security = security
|
||||
}
|
||||
connectionsPerHost = 100
|
||||
autoConnectRetry = true
|
||||
|
|
1
nginx
1
nginx
|
@ -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/;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
1
todo
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue