From e2d633cda998323f73145009a1552fc34c131e66 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 5 Apr 2024 10:24:09 +0200 Subject: [PATCH] remove oauth dependency to user with Scoped[U: UserIdOf] --- app/controllers/Account.scala | 4 +- app/controllers/Challenge.scala | 24 +++++----- app/controllers/DgtCtrl.scala | 5 +- app/controllers/Game.scala | 4 +- app/controllers/LilaController.scala | 3 +- app/controllers/OAuth.scala | 4 +- app/controllers/OAuthToken.scala | 8 ++-- app/controllers/User.scala | 2 +- app/http/RequestContext.scala | 4 +- build.sbt | 4 +- .../src/main/ChallengeBulkSetup.scala | 22 +++++---- .../core/src/main/lilaism/LilaUserId.scala | 1 + modules/core/src/main/user.scala | 2 + modules/oauth/src/main/AccessTokenApi.scala | 46 +++++++++---------- .../oauth/src/main/AuthorizationRequest.scala | 17 ++----- modules/oauth/src/main/Env.scala | 4 +- modules/oauth/src/main/OAuthScope.scala | 7 ++- modules/oauth/src/main/OAuthServer.scala | 32 +++++++------ modules/oauth/src/main/OAuthTokenForm.scala | 1 - modules/security/src/main/SecurityApi.scala | 11 +++-- modules/user/src/main/User.scala | 1 - modules/user/src/main/UserApi.scala | 4 +- modules/user/src/main/UserRepo.scala | 4 +- 23 files changed, 110 insertions(+), 104 deletions(-) diff --git a/app/controllers/Account.scala b/app/controllers/Account.scala index bab6165f3291e..a040ab0226d2a 100644 --- a/app/controllers/Account.scala +++ b/app/controllers/Account.scala @@ -307,8 +307,8 @@ final class Account( for _ <- env.security.api.dedup(me, req) sessions <- env.security.api.locatedOpenSessions(me, 50) - clients <- env.oAuth.tokenApi.listClients(me, 50) - personalAccessTokens <- env.oAuth.tokenApi.countPersonal(me) + clients <- env.oAuth.tokenApi.listClients(50) + personalAccessTokens <- env.oAuth.tokenApi.countPersonal currentSessionId = ~env.security.api.reqSessionId(req) page <- renderPage: html.account.security(me, sessions, currentSessionId, clients, personalAccessTokens) diff --git a/app/controllers/Challenge.scala b/app/controllers/Challenge.scala index 0b880e3b65744..6c185acd7eb1d 100644 --- a/app/controllers/Challenge.scala +++ b/app/controllers/Challenge.scala @@ -10,17 +10,18 @@ import lila.challenge.Challenge.Id as ChallengeId import lila.core.{ Bearer, IpAddress, Preload } import lila.game.{ AnonCookie, Pov } -import lila.oauth.{ EndpointScopes, OAuthScope } +import lila.oauth.{ EndpointScopes, OAuthScope, OAuthServer } import lila.setup.ApiConfig import lila.core.socket.SocketVersion -import lila.user.User as UserModel +import lila.user.{ Me, User as UserModel } final class Challenge( env: Env, apiC: Api ) extends LilaController(env): - def api = env.challenge.api + def api = env.challenge.api + private given OAuthServer.FetchUser[Me] = env.user.repo.me def all = Auth { ctx ?=> me ?=> XhrOrRedirectHome: @@ -214,8 +215,8 @@ final class Challenge( Bearer.from(get("opponentToken")) match case Some(bearer) => val required = OAuthScope.select(_.Challenge.Write).into(EndpointScopes) - env.oAuth.server.auth(bearer, required, ctx.req.some).map { - case Right(access) if pov.opponent.isUser(access.user) => + env.oAuth.server.auth[Me](bearer, required, ctx.req.some).map { + case Right(access) if pov.opponent.isUser(access.me) => lila.common.Bus.publish(Tell(id.value, AbortForce), "roundSocket") jsonOkResult case Right(_) => BadRequest(jsonError("Not the opponent token")) @@ -237,20 +238,21 @@ final class Challenge( val accepted = OAuthScope.select(_.Challenge.Write).into(EndpointScopes) (Bearer.from(get("token1")), Bearer.from(get("token2"))) .mapN: - env.oAuth.server.authBoth(accepted, req) + env.oAuth.server.authBoth[Me](accepted, req) .so: _.flatMap: case Left(e) => handleScopedFail(accepted, e) case Right((u1, u2)) => env.game.gameRepo .game(id) - .flatMapz { g => - env.round.proxyRepo.upgradeIfPresent(g).dmap(some).dmap(_.filter(_.hasUserIds(u1.id, u2.id))) - } - .orNotFound { game => + .flatMapz: g => + env.round.proxyRepo + .upgradeIfPresent(g) + .dmap(some) + .dmap(_.filter(_.hasUserIds(u1.userId, u2.userId))) + .orNotFound: game => env.round.tellRound(game.id, lila.core.round.StartClock) jsonOkResult - } private val ChallengeIpRateLimit = lila.memo.RateLimit[IpAddress]( 500, diff --git a/app/controllers/DgtCtrl.scala b/app/controllers/DgtCtrl.scala index 37eac08e29abc..2d8ecfc95314d 100644 --- a/app/controllers/DgtCtrl.scala +++ b/app/controllers/DgtCtrl.scala @@ -23,7 +23,6 @@ final class DgtCtrl(env: Env) extends LilaController(env): description = "DGT board automatic token", scopes = dgtScopes.value.map(_.key) ), - me, isStudent = false ) >> env.pref.api.saveTag(me, _.dgt, true) @@ -47,5 +46,5 @@ final class DgtCtrl(env: Env) extends LilaController(env): _.Board.Play ) - private def findToken(using me: Me) = - env.oAuth.tokenApi.findCompatiblePersonal(me, dgtScopes) + private def findToken(using Me) = + env.oAuth.tokenApi.findCompatiblePersonal(dgtScopes) diff --git a/app/controllers/Game.scala b/app/controllers/Game.scala index a0bad0f15255e..499269cdea027 100644 --- a/app/controllers/Game.scala +++ b/app/controllers/Game.scala @@ -64,7 +64,7 @@ final class Game(env: Env, apiC: => Api) extends LilaController(env): WithVs: vs => env.security.ipTrust .throttle(MaxPerSecond: - if ctx.is(lila.user.User.explorerId) then env.apiExplorerGamesPerSecond.get() + if ctx.is(UserId.explorer) then env.apiExplorerGamesPerSecond.get() else if ctx.is(user) then 60 else if ctx.isOAuth then 30 // bonus for oauth logged in only (not for CSRF) else 25 @@ -91,7 +91,7 @@ final class Game(env: Env, apiC: => Api) extends LilaController(env): ongoing = getBool("ongoing") || !finished, finished = finished ) - if ctx.is(lila.user.User.explorerId) then + if ctx.is(UserId.explorer) then Ok.chunked(env.api.gameApiV2.exportByUser(config)) .pipe(noProxyBuffer) .as(gameContentType(config)) diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala index 7ca8822033f39..f52b28ffb475f 100644 --- a/app/controllers/LilaController.scala +++ b/app/controllers/LilaController.scala @@ -11,6 +11,7 @@ import lila.common.{ HTTPRequest, config } import lila.i18n.LangPicker import lila.oauth.{ EndpointScopes, OAuthScope, OAuthScopes, OAuthServer, TokenScopes } import lila.security.Permission +import lila.user.Me abstract private[controllers] class LilaController(val env: Env) extends BaseController @@ -227,7 +228,7 @@ abstract private[controllers] class LilaController(val env: Env) f(using ctx)(using scoped.me) private def handleScopedCommon(selectors: Seq[OAuthScope.Selector])(using req: RequestHeader)( - f: OAuthScope.Scoped => Fu[Result] + f: OAuthScope.Scoped[Me] => Fu[Result] ) = val accepted = OAuthScope.select(selectors).into(EndpointScopes) env.security.api.oauthScoped(req, accepted).flatMap { diff --git a/app/controllers/OAuth.scala b/app/controllers/OAuth.scala index 0207fe7aa6e97..4ae71a0dd06f3 100644 --- a/app/controllers/OAuth.scala +++ b/app/controllers/OAuth.scala @@ -120,12 +120,12 @@ final class OAuth(env: Env, apiC: => Api) extends LilaController(env): private val revokeClientForm = Form(single("origin" -> text)) - def revokeClient = AuthBody { ctx ?=> me ?=> + def revokeClient = AuthBody { ctx ?=> _ ?=> revokeClientForm .bindFromRequest() .fold( _ => BadRequest, - origin => env.oAuth.tokenApi.revokeByClientOrigin(origin, me).inject(NoContent) + origin => env.oAuth.tokenApi.revokeByClientOrigin(origin).inject(NoContent) ) } diff --git a/app/controllers/OAuthToken.scala b/app/controllers/OAuthToken.scala index 49d3f3f38f6d9..844b5553819eb 100644 --- a/app/controllers/OAuthToken.scala +++ b/app/controllers/OAuthToken.scala @@ -11,7 +11,7 @@ final class OAuthToken(env: Env) extends LilaController(env): def index = Auth { ctx ?=> me ?=> Ok.pageAsync: - tokenApi.listPersonal(me).map(html.oAuth.token.index(_)) + tokenApi.listPersonal.map(html.oAuth.token.index(_)) } def create = Auth { ctx ?=> me ?=> @@ -31,11 +31,11 @@ final class OAuthToken(env: Env) extends LilaController(env): err => BadRequest.page(html.oAuth.token.create(err, me)), setup => tokenApi - .create(setup, me, env.clas.studentCache.isStudent(me)) + .create(setup, env.clas.studentCache.isStudent(me)) .inject(Redirect(routes.OAuthToken.index).flashSuccess) ) } - def delete(id: String) = Auth { _ ?=> me ?=> - tokenApi.revokeById(AccessToken.Id(id), me).inject(Redirect(routes.OAuthToken.index).flashSuccess) + def delete(id: String) = Auth { _ ?=> _ ?=> + tokenApi.revokeById(AccessToken.Id(id)).inject(Redirect(routes.OAuthToken.index).flashSuccess) } diff --git a/app/controllers/User.scala b/app/controllers/User.scala index c00297d174d9f..b7026f1dce3c5 100644 --- a/app/controllers/User.scala +++ b/app/controllers/User.scala @@ -419,7 +419,7 @@ final class User( .inject(html.user.mod.assessments(user, as)) } - val boardTokens = env.oAuth.tokenApi.usedBoardApi(user).map(html.user.mod.boardTokens) + val boardTokens = env.oAuth.tokenApi.usedBoardApi.map(html.user.mod.boardTokens) val teacher = env.clas.api.clas.countOf(user).map(html.user.mod.teacher(user)) diff --git a/app/http/RequestContext.scala b/app/http/RequestContext.scala index c7ff1bcd49066..b97a4f1351cbe 100644 --- a/app/http/RequestContext.scala +++ b/app/http/RequestContext.scala @@ -27,7 +27,7 @@ trait RequestContext(using Executor): pref <- env.pref.api.get(userCtx.me, req) yield BodyContext(req, lang, userCtx, pref) - def oauthContext(scoped: OAuthScope.Scoped)(using req: RequestHeader): Fu[Context] = + def oauthContext(scoped: OAuthScope.Scoped[Me])(using req: RequestHeader): Fu[Context] = val lang = getAndSaveLang(req, scoped.me.some) val userCtx = LoginContext(scoped.me.some, false, none, scoped.scopes.some) env.pref.api @@ -35,7 +35,7 @@ trait RequestContext(using Executor): .map: Context(req, lang, userCtx, _) - def oauthBodyContext[A](scoped: OAuthScope.Scoped)(using req: Request[A]): Fu[BodyContext[A]] = + def oauthBodyContext[A](scoped: OAuthScope.Scoped[Me])(using req: Request[A]): Fu[BodyContext[A]] = val lang = getAndSaveLang(req, scoped.me.some) val userCtx = LoginContext(scoped.me.some, false, none, scoped.scopes.some) env.pref.api diff --git a/build.sbt b/build.sbt index 561deaacbef94..a45e40ba29652 100644 --- a/build.sbt +++ b/build.sbt @@ -315,12 +315,12 @@ lazy val irwin = module("irwin", ) lazy val oauth = module("oauth", - Seq(user), + Seq(memo), reactivemongo.bundle ) lazy val security = module("security", - Seq(oauth, mailer), + Seq(oauth, user, mailer), Seq(maxmind, hasher, uaparser) ++ tests.bundle ++ reactivemongo.bundle ) diff --git a/modules/challenge/src/main/ChallengeBulkSetup.scala b/modules/challenge/src/main/ChallengeBulkSetup.scala index c66c0ee365272..a40cf222eb4ab 100644 --- a/modules/challenge/src/main/ChallengeBulkSetup.scala +++ b/modules/challenge/src/main/ChallengeBulkSetup.scala @@ -14,7 +14,7 @@ import lila.game.IdGenerator import lila.core.game.GameRule import lila.oauth.{ EndpointScopes, OAuthScope, OAuthServer } import lila.core.perf.PerfType -import lila.user.User +import lila.user.{ User, Me } final class ChallengeBulkSetup(setupForm: lila.core.setup.SetupForm): @@ -80,10 +80,11 @@ final class ChallengeBulkSetup(setupForm: lila.core.setup.SetupForm): ) ) -final class ChallengeBulkSetupApi(oauthServer: OAuthServer, idGenerator: IdGenerator)(using - Executor, - akka.stream.Materializer -): +final class ChallengeBulkSetupApi( + oauthServer: OAuthServer, + idGenerator: IdGenerator, + userRepo: lila.user.UserRepo +)(using Executor, akka.stream.Materializer): import ChallengeBulkSetup.* @@ -96,18 +97,21 @@ final class ChallengeBulkSetupApi(oauthServer: OAuthServer, idGenerator: IdGener ) def apply(data: BulkFormData, me: User): Fu[Result] = + given OAuthServer.FetchUser[Me] = userRepo.me Source(extractTokenPairs(data.tokens)) .mapConcat: (whiteToken, blackToken) => List(whiteToken, blackToken) // flatten now, re-pair later! .mapAsync(8): token => - oauthServer.auth(token, OAuthScope.select(_.Challenge.Write).into(EndpointScopes), none).map { - _.left.map { BadToken(token, _) } - } + oauthServer + .auth[Me](token, OAuthScope.select(_.Challenge.Write).into(EndpointScopes), none) + .map { + _.left.map { BadToken(token, _) } + } .runFold[Either[List[BadToken], List[UserId]]](Right(Nil)): case (Left(bads), Left(bad)) => Left(bad :: bads) case (Left(bads), _) => Left(bads) case (Right(_), Left(bad)) => Left(bad :: Nil) - case (Right(users), Right(scoped)) => Right(scoped.me :: users) + case (Right(users), Right(scoped)) => Right(scoped.me.userId :: users) .flatMap: case Left(errors) => fuccess(Left(ScheduleError.BadTokens(errors.reverse))) case Right(allPlayers) => diff --git a/modules/core/src/main/lilaism/LilaUserId.scala b/modules/core/src/main/lilaism/LilaUserId.scala index 31bd95192af59..71c87449813bc 100644 --- a/modules/core/src/main/lilaism/LilaUserId.scala +++ b/modules/core/src/main/lilaism/LilaUserId.scala @@ -24,6 +24,7 @@ trait LilaUserId: val lichess: UserId = "lichess" val lichessAsMe: MyId = lichess.into(MyId) val ghost: UserId = "ghost" + val explorer: UserId = "openingexplorer" // specialized UserIds like Coach.Id trait OpaqueUserId[A] extends OpaqueString[A]: diff --git a/modules/core/src/main/user.scala b/modules/core/src/main/user.scala index 03c17586a7590..df3ee850edf4a 100644 --- a/modules/core/src/main/user.scala +++ b/modules/core/src/main/user.scala @@ -72,6 +72,8 @@ trait UserApi: def isKid[U: UserIdOf](id: U): Fu[Boolean] def isTroll(id: UserId): Fu[Boolean] def isBot(id: UserId): Fu[Boolean] + def filterDisabled(userIds: Iterable[UserId]): Fu[Set[UserId]] + def isManaged(id: UserId): Fu[Boolean] trait LightUserApiMinimal: val async: LightUser.Getter diff --git a/modules/oauth/src/main/AccessTokenApi.scala b/modules/oauth/src/main/AccessTokenApi.scala index 39cb0f2be355b..830e6e3688fcd 100644 --- a/modules/oauth/src/main/AccessTokenApi.scala +++ b/modules/oauth/src/main/AccessTokenApi.scala @@ -4,13 +4,13 @@ import reactivemongo.api.bson.* import lila.core.Bearer import lila.db.dsl.{ *, given } -import lila.user.{ User, UserRepo } import lila.core.actorApi.oauth.TokenRevoke +import lila.core.user.MyId final class AccessTokenApi( coll: Coll, cacheApi: lila.memo.CacheApi, - userRepo: UserRepo + userApi: lila.core.user.UserApi )(using Executor): import OAuthScope.given @@ -30,14 +30,14 @@ final class AccessTokenApi( _ <- coll.insert.one(token) yield token - def create(setup: OAuthTokenForm.Data, me: User, isStudent: Boolean): Fu[AccessToken] = - (fuccess(isStudent) >>| userRepo.isManaged(me.id)).flatMap { noBot => + def create(setup: OAuthTokenForm.Data, isStudent: Boolean)(using me: MyId): Fu[AccessToken] = + (fuccess(isStudent) >>| userApi.isManaged(me)).flatMap { noBot => val plain = Bearer.randomPersonal() createAndRotate: AccessToken( id = AccessToken.Id.from(plain), plain = plain, - userId = me.id, + userId = me, description = setup.description.some, createdAt = nowInstant.some, scopes = TokenScopes: @@ -68,9 +68,9 @@ final class AccessTokenApi( def adminChallengeTokens( setup: OAuthTokenForm.AdminChallengeTokensData, - admin: User + admin: lila.core.user.User ): Fu[Map[UserId, AccessToken]] = - userRepo + userApi .enabledByIds(setup.usernames) .flatMap: users => val scope = OAuthScope.Challenge.Write @@ -99,51 +99,51 @@ final class AccessTokenApi( .map(user.id -> _) .map(_.toMap) - def listPersonal(user: User): Fu[List[AccessToken]] = + def listPersonal(using me: MyId): Fu[List[AccessToken]] = coll .find: $doc( - F.userId -> user.id, + F.userId -> me, F.clientOrigin -> $exists(false) ) .sort($sort.desc(F.createdAt)) // c.f. isBrandNew .cursor[AccessToken]() .list(100) - def usedBoardApi(user: User): Fu[List[AccessToken]] = + def usedBoardApi(using me: MyId): Fu[List[AccessToken]] = coll .find: $doc( F.scopes -> OAuthScope.Board.Play.key, F.usedAt.$exists(true), - F.userId -> user.id + F.userId -> me ) .sort($sort.desc(F.createdAt)) .cursor[AccessToken]() .list(30) - def countPersonal(user: User): Fu[Int] = + def countPersonal(using me: MyId): Fu[Int] = coll.countSel: $doc( - F.userId -> user.id, + F.userId -> me, F.clientOrigin -> $exists(false) ) - def findCompatiblePersonal(user: User, scopes: OAuthScopes): Fu[Option[AccessToken]] = + def findCompatiblePersonal(scopes: OAuthScopes)(using me: MyId): Fu[Option[AccessToken]] = coll.one[AccessToken]: $doc( - F.userId -> user.id, + F.userId -> me, F.clientOrigin -> $exists(false), F.scopes.$all(scopes.value) ) - def listClients(user: User, limit: Int): Fu[List[AccessTokenApi.Client]] = + def listClients(limit: Int)(using me: MyId): Fu[List[AccessTokenApi.Client]] = coll .aggregateList(limit): framework => import framework.* Match( $doc( - F.userId -> user.id, + F.userId -> me, F.clientOrigin -> $exists(true) ) ) -> List( @@ -163,21 +163,21 @@ final class AccessTokenApi( scopes <- doc.getAsOpt[List[OAuthScope]](F.scopes)(using collectionReader) yield AccessTokenApi.Client(origin, usedAt, scopes) - def revokeById(id: AccessToken.Id, user: User): Funit = + def revokeById(id: AccessToken.Id)(using me: MyId): Funit = coll.delete .one: $doc( F.id -> id, - F.userId -> user.id + F.userId -> me ) .void .andDo(onRevoke(id)) - def revokeByClientOrigin(clientOrigin: String, user: User): Funit = + def revokeByClientOrigin(clientOrigin: String)(using me: MyId): Funit = coll .find( $doc( - F.userId -> user.id, + F.userId -> me, F.clientOrigin -> clientOrigin ), $doc(F.id -> 1).some @@ -189,7 +189,7 @@ final class AccessTokenApi( coll.delete .one: $doc( - F.userId -> user.id, + F.userId -> me, F.clientOrigin -> clientOrigin ) .map(_ => invalidate.flatMap(_.getAsOpt[AccessToken.Id](F.id)).foreach(onRevoke)) @@ -207,7 +207,7 @@ final class AccessTokenApi( readPref = _.sec )(_.id) .flatMap: tokens => - userRepo.filterDisabled(tokens.flatten.map(_.userId)).map { closedUserIds => + userApi.filterDisabled(tokens.flatten.map(_.userId)).map { closedUserIds => val openTokens = tokens.map(_.filter(token => !closedUserIds(token.userId))) bearers.zip(openTokens).toMap } diff --git a/modules/oauth/src/main/AuthorizationRequest.scala b/modules/oauth/src/main/AuthorizationRequest.scala index 91adb7650522e..610db31ba8bdb 100644 --- a/modules/oauth/src/main/AuthorizationRequest.scala +++ b/modules/oauth/src/main/AuthorizationRequest.scala @@ -1,7 +1,5 @@ package lila.oauth -import lila.user.User - object AuthorizationRequest: import Protocol.* @@ -72,7 +70,7 @@ object AuthorizationRequest: lazy val isDanger = scopes.intersects(OAuthScope.dangerList) && !trusted && !looksLikeLichessMobile def authorize( - user: User, + user: UserId, legacy: (ClientId, RedirectUri) => Fu[Option[LegacyClientApi.HashedClientSecret]] ): Fu[Either[Error, Authorized]] = codeChallengeMethod @@ -92,14 +90,7 @@ object AuthorizationRequest: challenge <- challenge scopes <- validScopes _ <- responseType.toRight(Error.ResponseTypeRequired).flatMap(ResponseType.from) - yield Authorized( - clientId, - redirectUri, - state, - user.id, - scopes, - challenge - ) + yield Authorized(clientId, redirectUri, state, user, scopes, challenge) case class Authorized( clientId: ClientId, @@ -111,8 +102,8 @@ object AuthorizationRequest: ): def redirectUrl(code: AuthorizationCode) = redirectUri.code(code, state) - def logPrompt(prompt: Prompt, me: Option[User])(using req: play.api.mvc.RequestHeader) = + def logPrompt(prompt: Prompt, me: Option[UserId])(using req: play.api.mvc.RequestHeader) = if prompt.mimicsLichessMobile then val reqInfo = lila.common.HTTPRequest.print(req) - logger.warn(s"OAuth prompt looks like lichess mobile: ${me.fold("anon")(_.username)} $reqInfo") + logger.warn(s"OAuth prompt looks like lichess mobile: ${me.fold("anon")(_.value)} $reqInfo") diff --git a/modules/oauth/src/main/Env.scala b/modules/oauth/src/main/Env.scala index 89a18c1e6a295..d52e64faac1c6 100644 --- a/modules/oauth/src/main/Env.scala +++ b/modules/oauth/src/main/Env.scala @@ -12,7 +12,7 @@ import lila.memo.SettingStore.Strings.given @Module final class Env( cacheApi: lila.memo.CacheApi, - userRepo: lila.user.UserRepo, + userApi: lila.core.user.UserApi, settingStore: lila.memo.SettingStore.Builder, appConfig: Configuration, db: lila.db.Db @@ -28,7 +28,7 @@ final class Env( lazy val authorizationApi = AuthorizationApi(db(CollName("oauth2_authorization"))) - lazy val tokenApi = AccessTokenApi(db(CollName("oauth2_access_token")), cacheApi, userRepo) + lazy val tokenApi = AccessTokenApi(db(CollName("oauth2_access_token")), cacheApi, userApi) private val mobileSecret = appConfig.get[Secret]("oauth.mobile.secret").taggedWith[MobileSecret] lazy val server = wire[OAuthServer] diff --git a/modules/oauth/src/main/OAuthScope.scala b/modules/oauth/src/main/OAuthScope.scala index 52fa74c505382..91f931fcb49c6 100644 --- a/modules/oauth/src/main/OAuthScope.scala +++ b/modules/oauth/src/main/OAuthScope.scala @@ -3,7 +3,7 @@ package lila.oauth import cats.derived.* import lila.core.i18n.I18nKey import lila.core.i18n.I18nKey.{ oauthScope as trans } -import lila.user.User +import lila.core.user.User sealed abstract class OAuthScope(val key: String, val name: I18nKey): override def toString = s"Scope($key)" @@ -86,10 +86,9 @@ object OAuthScope: case object Mobile extends OAuthScope("web:mobile", I18nKey("Official Lichess mobile app")) case object Mod extends OAuthScope("web:mod", trans.webMod) - case class Scoped(me: lila.user.Me, scopes: TokenScopes): - def user: User = me.value + case class Scoped[U: UserIdOf](me: U, scopes: TokenScopes) - case class Access(scoped: Scoped, tokenId: AccessToken.Id): + case class Access[U: UserIdOf](scoped: Scoped[U], tokenId: AccessToken.Id): export scoped.* type Selector = OAuthScope.type => OAuthScope diff --git a/modules/oauth/src/main/OAuthServer.scala b/modules/oauth/src/main/OAuthServer.scala index c837c2874dee2..151cc40ea272b 100644 --- a/modules/oauth/src/main/OAuthServer.scala +++ b/modules/oauth/src/main/OAuthServer.scala @@ -7,19 +7,20 @@ import com.roundeights.hasher.Algo import lila.common.HTTPRequest import lila.core.{ Bearer, Strings } import lila.memo.SettingStore -import lila.user.{ User, UserRepo } +import lila.core.user.User import lila.core.config.Secret final class OAuthServer( tokenApi: AccessTokenApi, - userRepo: UserRepo, originBlocklist: SettingStore[Strings] @@ OriginBlocklist, mobileSecret: Secret @@ MobileSecret )(using Executor): import OAuthServer.* - def auth(req: RequestHeader, accepted: EndpointScopes): Fu[AccessResult] = + def auth[U: UserIdOf](req: RequestHeader, accepted: EndpointScopes)(using + FetchUser[U] + ): Fu[AccessResult[U]] = HTTPRequest .bearer(req) .fold(fufail(MissingAuthorizationHeader)): @@ -30,14 +31,18 @@ final class OAuthServer( .addEffect: res => monitorAuth(res.isRight) - def auth(tokenId: Bearer, accepted: EndpointScopes, andLogReq: Option[RequestHeader]): Fu[AccessResult] = + def auth[U: UserIdOf]( + tokenId: Bearer, + accepted: EndpointScopes, + andLogReq: Option[RequestHeader] + )(using fetchUser: FetchUser[U]): Fu[AccessResult[U]] = getTokenFromSignedBearer(tokenId) .orFailWith(NoSuchToken) .flatMap { case at if !accepted.isEmpty && !accepted.compatible(at.scopes) => fufail(MissingScope(at.scopes)) case at => - userRepo.me(at.userId).flatMap { + fetchUser(at.userId).flatMap { case None => fufail(NoSuchUser) case Some(u) => val blocked = @@ -45,11 +50,11 @@ final class OAuthServer( andLogReq .filter: req => blocked || { - u.userId != User.explorerId && !HTTPRequest.looksLikeLichessBot(req) + u.isnt(UserId.explorer) && !HTTPRequest.looksLikeLichessBot(req) } .foreach: req => logger.debug: - s"${if blocked then "block" else "auth"} ${at.clientOrigin | "-"} as ${u.username} ${HTTPRequest.print(req).take(200)}" + s"${if blocked then "block" else "auth"} ${at.clientOrigin | "-"} as ${u.id} ${HTTPRequest.print(req).take(200)}" if blocked then fufail(OriginBlocked) else fuccess(OAuthScope.Access(OAuthScope.Scoped(u, at.scopes), at.tokenId)) } @@ -59,10 +64,10 @@ final class OAuthServer( Left(e) } - def authBoth(scopes: EndpointScopes, req: RequestHeader)( + def authBoth[U: UserIdOf](scopes: EndpointScopes, req: RequestHeader)( token1: Bearer, token2: Bearer - ): Fu[Either[AuthError, (User, User)]] = for + )(using FetchUser[U]): Fu[Either[AuthError, (U, U)]] = for auth1 <- auth(token1, scopes, req.some) auth2 <- auth(token2, scopes, req.some) yield for @@ -71,11 +76,11 @@ final class OAuthServer( result <- if user1.me.is(user2.me) then Left(OneUserWithTwoTokens) - else Right(user1.user -> user2.user) + else Right(user1.me -> user2.me) yield result val UaUserRegex = """(?:user|as):\s?([\w\-]{3,31})""".r - private def checkOauthUaUser(req: RequestHeader)(access: AccessResult): AccessResult = + private def checkOauthUaUser[U: UserIdOf](req: RequestHeader)(access: AccessResult[U]): AccessResult[U] = access.flatMap: a => HTTPRequest.userAgent(req).map(_.value) match case Some(UaUserRegex(u)) if a.me.isnt(UserStr(u)) => Left(UserAgentMismatch) @@ -99,8 +104,9 @@ final class OAuthServer( object OAuthServer: - type AccessResult = Either[AuthError, OAuthScope.Access] - type AuthResult = Either[AuthError, OAuthScope.Scoped] + type FetchUser[U] = UserId => Fu[Option[U]] + type AccessResult[U] = Either[AuthError, OAuthScope.Access[U]] + type AuthResult[U] = Either[AuthError, OAuthScope.Scoped[U]] sealed abstract class AuthError(val message: String) extends lila.core.lilaism.LilaException case object MissingAuthorizationHeader extends AuthError("Missing authorization header") diff --git a/modules/oauth/src/main/OAuthTokenForm.scala b/modules/oauth/src/main/OAuthTokenForm.scala index 2875790911b57..f4f64dee1a8f2 100644 --- a/modules/oauth/src/main/OAuthTokenForm.scala +++ b/modules/oauth/src/main/OAuthTokenForm.scala @@ -4,7 +4,6 @@ import play.api.data.* import play.api.data.Forms.* import lila.common.Form.cleanText -import lila.user.User object OAuthTokenForm: diff --git a/modules/security/src/main/SecurityApi.scala b/modules/security/src/main/SecurityApi.scala index 665e0e8a4676f..350f4bbe07f85 100644 --- a/modules/security/src/main/SecurityApi.scala +++ b/modules/security/src/main/SecurityApi.scala @@ -145,9 +145,10 @@ final class SecurityApi( def oauthScoped( req: RequestHeader, required: lila.oauth.EndpointScopes - ): Fu[OAuthServer.AuthResult] = + ): Fu[OAuthServer.AuthResult[Me]] = + given OAuthServer.FetchUser[Me] = userRepo.me oAuthServer - .auth(req, required) + .auth[Me](req, required) .addEffect: case Right(access) => upsertOauth(access, req) case _ => () @@ -155,14 +156,14 @@ final class SecurityApi( private object upsertOauth: private val sometimes = scalalib.cache.OnceEvery.hashCode[AccessToken.Id](1.hour) - def apply(access: OAuthScope.Access, req: RequestHeader): Unit = + def apply(access: OAuthScope.Access[Me], req: RequestHeader): Unit = if access.scoped.scopes.intersects(OAuthScope.relevantToMods) && sometimes(access.tokenId) then val mobile = Mobile.LichessMobileUa.parse(req) - store.upsertOAuth(access.user.id, access.tokenId, mobile, req) + store.upsertOAuth(access.me.userId, access.tokenId, mobile, req) private lazy val nonModRoles: Set[String] = Permission.nonModPermissions.map(_.dbKey) - private def stripRolesOfOAuthUser(scoped: OAuthScope.Scoped) = + private def stripRolesOfOAuthUser(scoped: OAuthScope.Scoped[Me]) = if scoped.scopes.has(_.Web.Mod) then scoped else scoped.copy(me = stripRolesOf(scoped.me)) diff --git a/modules/user/src/main/User.scala b/modules/user/src/main/User.scala index a0a7ec2645c0b..c3c609675fb74 100644 --- a/modules/user/src/main/User.scala +++ b/modules/user/src/main/User.scala @@ -159,7 +159,6 @@ object User: val broadcasterId = UserId("broadcaster") val irwinId = UserId("irwin") val kaladinId = UserId("kaladin") - val explorerId = UserId("openingexplorer") val lichess4545Id = UserId("lichess4545") val challengermodeId = UserId("challengermode") val watcherbotId = UserId("watcherbot") diff --git a/modules/user/src/main/UserApi.scala b/modules/user/src/main/UserApi.scala index 55f4462e51c3a..cd8feb37cdd6a 100644 --- a/modules/user/src/main/UserApi.scala +++ b/modules/user/src/main/UserApi.scala @@ -32,7 +32,9 @@ final class UserApi(userRepo: UserRepo, perfsRepo: UserPerfsRepo, cacheApi: Cach isKid, langOf, isBot, - isTroll + isTroll, + isManaged, + filterDisabled } // hit by game rounds diff --git a/modules/user/src/main/UserRepo.scala b/modules/user/src/main/UserRepo.scala index 766a2284eb99b..72658fc0955d6 100644 --- a/modules/user/src/main/UserRepo.scala +++ b/modules/user/src/main/UserRepo.scala @@ -53,8 +53,8 @@ final class UserRepo(c: Coll)(using Executor) extends lila.core.user.UserRepo(c) Left(LightUser.ghost).some } - def me[U: UserIdOf](u: U): Fu[Option[Me]] = - enabledById(u).dmap(Me.from(_)) + def me(id: UserId): Fu[Option[Me]] = enabledById(id).dmap(Me.from(_)) + def me[U: UserIdOf](u: U): Fu[Option[Me]] = me(u.id) def byEmail(email: NormalizedEmailAddress): Fu[Option[User]] = coll.one[User]($doc(F.email -> email)) def byPrevEmail(