encrypt IP addresses for non-admin moderators

pull/8351/head
Thibault Duplessis 2021-03-10 20:51:47 +01:00
parent 0290d578e2
commit 40265c3642
12 changed files with 94 additions and 41 deletions

View File

@ -197,6 +197,7 @@ final class EnvBoot(
lazy val mainDb: lila.db.Db = mongo.blockingDb("main", config.get[String]("mongodb.uri"))
lazy val imageRepo = new lila.db.ImageRepo(mainDb(CollName("image")))
lazy val symmetricCipher = new lila.common.SymmetricCipher(config.get[Secret]("api.symmetric_cipher.key"))
// wire all the lila modules
lazy val memo: lila.memo.Env = wire[lila.memo.Env]

View File

@ -221,6 +221,7 @@ final class Mod(
if (priv) perms.ViewPrivateComms else perms.Shadowban
} { implicit ctx => me =>
OptionFuOk(env.user.repo named username) { user =>
implicit val renderIp = env.mod.ipRender(me)
env.game.gameRepo
.recentPovsByUserFromSecondary(user, 80)
.mon(_.mod.comm.segment("recentPovs"))
@ -346,7 +347,7 @@ final class Mod(
}
def print(fh: String) =
SecureBody(_.PrintBan) { implicit ctx => _ =>
SecureBody(_.ViewPrintNoIP) { implicit ctx => _ =>
val hash = FingerHash(fh)
for {
uids <- env.security.api recentUserIdsByFingerHash hash
@ -363,8 +364,9 @@ final class Mod(
}
def singleIp(ip: String) =
SecureBody(_.IpBan) { implicit ctx => _ =>
IpAddress.from(ip) ?? { address =>
SecureBody(_.ViewPrintNoIP) { implicit ctx => me =>
implicit val renderIp = env.mod.ipRender(me)
env.mod.ipRender.decrypt(ip) ?? { address =>
for {
uids <- env.security.api recentUserIdsByIp address
users <- env.user.repo usersFromSecondary uids.reverse

View File

@ -362,6 +362,7 @@ final class User(
case UserModel.WithEmails(user, emails) =>
import html.user.{ mod => view }
import lila.app.ui.ScalatagsExtensions.LilaFragZero
implicit val renderIp = env.mod.ipRender(holder)
val nbOthers = getInt("nbOthers") | 100
@ -394,9 +395,9 @@ final class User(
html.user.mod.otherUsers(holder, user, _)
}
}
val identification = userLoginsFu map { spy =>
(Granter.canViewFp(holder, user) || Granter.canViewIp(holder, user)) ??
html.user.mod.identification(spy)
val identification = userLoginsFu map { logins =>
Granter.is(_.ViewPrintNoIP)(holder) ??
html.user.mod.identification(holder, logins)
}
val irwin = isGranted(_.MarkEngine) ?? env.irwin.api.reports.withPovs(user).map {
_ ?? { reps =>

View File

@ -1,14 +1,15 @@
package views.html.mod
import controllers.routes
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.richText
import lila.hub.actorApi.shutup.PublicSource
import lila.mod.IpRender.RenderIp
import lila.user.{ Holder, User }
import controllers.routes
object communication {
def apply(
@ -21,7 +22,7 @@ object communication {
history: List[lila.mod.Modlog],
logins: lila.security.UserLogins.TableData,
priv: Boolean
)(implicit ctx: Context) =
)(implicit ctx: Context, renderIp: RenderIp) =
views.html.base.layout(
title = u.username + " communications",
moreCss = frag(
@ -56,7 +57,7 @@ object communication {
),
isGranted(_.UserModView) option frag(
div(cls := "mod-zone none"),
views.html.user.mod.otherUsers(mod, u, logins)(ctx)(cls := "communication__logins")
views.html.user.mod.otherUsers(mod, u, logins)(ctx, renderIp)(cls := "communication__logins")
),
history.nonEmpty option frag(
h2("Moderation history"),

View File

@ -7,6 +7,7 @@ import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.IpAddress
import lila.security.FingerHash
import lila.mod.IpRender.RenderIp
import controllers.routes
@ -80,7 +81,7 @@ object search {
users: List[lila.user.User.WithEmails],
uas: List[String],
blocked: Boolean
)(implicit ctx: Context) =
)(implicit ctx: Context, renderIp: RenderIp) =
views.html.base.layout(
title = "IP address",
moreCss = cssTag("mod.misc")
@ -89,7 +90,7 @@ object search {
views.html.mod.menu("search"),
div(cls := "mod-search page-menu__content box")(
div(cls := "box__top")(
h1("IP address: ", address.value),
h1("IP address: ", renderIp(address)),
postForm(cls := "box__top__actions", action := routes.Mod.singleIpBan(!blocked, address.value))(
submitButton(
cls := List(
@ -99,7 +100,7 @@ object search {
)(if (blocked) "Banned" else "Ban this IP")
)
),
div(cls := "box__pad")(
isGranted(_.Admin) option div(cls := "box__pad")(
h2("User agents"),
ul(uas map { ua =>
li(ua)

View File

@ -12,6 +12,7 @@ import lila.playban.RageSit
import lila.security.Granter
import lila.security.{ Permission, UserLogins }
import lila.user.{ Holder, User }
import lila.mod.IpRender.RenderIp
object mod {
private def mzSection(key: String) = div(id := s"mz_$key", cls := "mz-section")
@ -517,7 +518,10 @@ object mod {
if (nb > 0) td(cls := "i", dataSort := nb)(content)
else td
def otherUsers(mod: Holder, u: User, data: UserLogins.TableData)(implicit ctx: Context): Tag = {
def otherUsers(mod: Holder, u: User, data: UserLogins.TableData)(implicit
ctx: Context,
renderIp: RenderIp
): Tag = {
import data._
mzSection("others")(
table(cls := "slist")(
@ -551,7 +555,7 @@ object mod {
val userNotes =
notes.filter(n => n.to == o.id && (ctx.me.exists(n.isFrom) || isGranted(_.Admin)))
tr(
dataTags := s"${other.ips.mkString(" ")} ${other.fps.mkString(" ")}",
dataTags := s"${other.ips.map(renderIp).mkString(" ")} ${other.fps.mkString(" ")}",
cls := (o == u) option "same"
)(
if (o == u || Granter.canViewAltUsername(mod, o))
@ -602,7 +606,10 @@ object mod {
)
}
def identification(spy: UserLogins)(implicit ctx: Context): Frag = {
def identification(mod: Holder, logins: UserLogins)(implicit
ctx: Context,
renderIp: RenderIp
): Frag = {
val canIpBan = isGranted(_.IpBan)
val canFpBan = isGranted(_.PrintBan)
mzSection("identification")(
@ -617,7 +624,7 @@ object mod {
)
),
tbody(
spy.distinctLocations.toList
logins.distinctLocations.toList
.sortBy(-_.seconds)
.map { loc =>
tr(
@ -635,7 +642,7 @@ object mod {
table(cls := "slist slist--sort")(
thead(
tr(
th(pluralize("Device", spy.uas.size)),
th(pluralize("Device", logins.uas.size)),
th("OS"),
th("Client"),
sortNumberTh("Date"),
@ -643,7 +650,7 @@ object mod {
)
),
tbody(
spy.uas
logins.uas
.sortBy(-_.seconds)
.map { ua =>
import ua.value.client._
@ -666,7 +673,7 @@ object mod {
table(cls := "slist spy_filter slist--sort")(
thead(
tr(
th(pluralize("IP", spy.prints.size)),
th(pluralize("IP", logins.prints.size)),
sortNumberTh("Alts"),
th,
sortNumberTh("Date"),
@ -674,9 +681,10 @@ object mod {
)
),
tbody(
spy.ips.sortBy(ip => (-ip.alts.score, -ip.ip.seconds)).map { ip =>
logins.ips.sortBy(ip => (-ip.alts.score, -ip.ip.seconds)).map { ip =>
val renderedIp = renderIp(ip.ip.value)
tr(cls := ip.blocked option "blocked")(
td(a(href := routes.Mod.singleIp(ip.ip.value.value))(ip.ip.value)),
td(a(href := routes.Mod.singleIp(renderedIp))(renderedIp take 16)),
td(dataSort := ip.alts.score)(altMarks(ip.alts)),
td(ip.proxy option span(cls := "proxy")("PROXY")),
td(dataSort := ip.ip.date.getMillis)(momentFromNowServer(ip.ip.date)),
@ -698,14 +706,14 @@ object mod {
table(cls := "slist spy_filter slist--sort")(
thead(
tr(
th(pluralize("Print", spy.prints.size)),
th(pluralize("Print", logins.prints.size)),
sortNumberTh("Alts"),
sortNumberTh("Date"),
canFpBan option sortNumberTh
)
),
tbody(
spy.prints.sortBy(fp => (-fp.alts.score, -fp.fp.seconds)).map { fp =>
logins.prints.sortBy(fp => (-fp.alts.score, -fp.fp.seconds)).map { fp =>
tr(cls := fp.banned option "blocked")(
td(a(href := routes.Mod.print(fp.fp.value.value))(fp.fp.value)),
td(dataSort := fp.alts.score)(altMarks(fp.alts)),

View File

@ -81,6 +81,7 @@ api {
endpoint = "http://monitor.lichess.ovh:8086/write?db=events"
env = "dev"
}
symmetric_cipher.key = "definitelynotthesameinprod"
}
accessibility {
blind {

View File

@ -46,11 +46,16 @@ final class SymmetricCipher(secret: Secret) {
def encrypt(text: String): Try[String] =
bytes.encrypt(text.getBytes).map(Base64.getEncoder.encodeToString)
def decrypt(input: Array[Byte]): Try[Array[Byte]] =
bytes.decrypt(input)
def decrypt(encryptedBase64String: String): Try[String] =
decrypt(Base64.getDecoder.decode(encryptedBase64String)).map(_.map(_.toChar).mkString)
bytes.decrypt(Base64.getDecoder.decode(encryptedBase64String)).map(_.map(_.toChar).mkString)
}
object base64UrlFriendly {
def encrypt(text: String): Try[String] =
base64.encrypt(text).map(_.replace("/", "_"))
def decrypt(encrypted: String): Try[String] =
base64.decrypt(encrypted.replace("_", "/"))
}
object hex {

View File

@ -40,7 +40,8 @@ final class Env(
noteApi: lila.user.NoteApi,
cacheApi: lila.memo.CacheApi,
slackApi: lila.irc.SlackApi,
msgApi: lila.msg.MsgApi
msgApi: lila.msg.MsgApi,
symmetricCipher: lila.common.SymmetricCipher
)(implicit
ec: scala.concurrent.ExecutionContext,
system: ActorSystem
@ -78,6 +79,8 @@ final class Env(
lazy val presets = wire[ModPresetsApi]
lazy val ipRender = wire[IpRender]
private lazy val sandbagWatch = wire[SandbagWatch]
lila.common.Bus.subscribeFuns(

View File

@ -0,0 +1,36 @@
package lila.mod
import com.github.blemale.scaffeine.Cache
import com.github.blemale.scaffeine.LoadingCache
import lila.common.IpAddress
import lila.common.SymmetricCipher
import lila.memo.CacheApi
import lila.security.Granter
import lila.user.Holder
object IpRender {
type Raw = String
type Rendered = String
type RenderIp = IpAddress => Rendered
}
final class IpRender(cipher: SymmetricCipher) {
import IpRender._
def apply(mod: Holder): RenderIp = if (Granter.is(_.Admin)(mod)) visible else encrypted
val visible = (ip: IpAddress) => ip.value
val encrypted = (ip: IpAddress) => cache.get(ip.value)
def decrypt(str: String) = IpAddress from {
cipher.base64UrlFriendly.decrypt(str) getOrElse str
}
private val cache: LoadingCache[Raw, Rendered] = CacheApi.scaffeineNoScheduler
.maximumSize(4096)
.build((raw: Raw) => cipher.base64UrlFriendly.encrypt(raw).get)
}

View File

@ -34,15 +34,6 @@ object Granter {
}
}
def canViewFp(mod: Holder, user: User): Boolean =
is(_.PrintBan)(mod) || is(_.Hunter)(mod)
def canViewIp(mod: Holder, user: User): Boolean =
is(_.IpBan)(mod)
def canViewEmail(mod: Holder, user: User): Boolean =
is(_.Admin)(mod)
def canViewAltUsername(mod: Holder, user: User): Boolean =
is(_.Admin)(mod) || {
(is(_.Hunter)(mod) && user.marks.engine) ||

View File

@ -25,8 +25,9 @@ object Permission {
case object SetKidMode extends Permission("SET_KID_MODE", List(UserModView), "Set Kid Mode")
case object MarkEngine extends Permission("ADJUST_CHEATER", List(UserModView), "Mark as cheater")
case object MarkBooster extends Permission("ADJUST_BOOSTER", List(UserModView), "Mark as booster")
case object IpBan extends Permission("IP_BAN", List(UserModView), "IP ban")
case object IpBan extends Permission("IP_BAN", List(UserModView, ViewPrintNoIP), "IP ban")
case object PrintBan extends Permission("PRINT_BAN", List(UserModView), "Print ban")
case object ViewPrintNoIP extends Permission("VIEW_PRINT_NOIP", "View Print & NoIP")
case object DisableTwoFactor extends Permission("DISABLE_2FA", "Disable 2FA")
case object CloseAccount extends Permission("CLOSE_ACCOUNT", List(UserModView), "Close/reopen account")
case object SetTitle extends Permission("SET_TITLE", List(UserModView), "Set/unset title")
@ -91,7 +92,8 @@ object Permission {
UserSearch,
RemoveRanking,
ModMessage,
ModNote
ModNote,
ViewPrintNoIP
),
"Hunter"
)
@ -109,7 +111,8 @@ object Permission {
ModMessage,
SeeReport,
ModLog,
ModNote
ModNote,
ViewPrintNoIP
),
"Shusher"
)