From 12a1410ee0e490dba79f04010af469e47290b1c2 Mon Sep 17 00:00:00 2001 From: "Frank S. Thomas" Date: Fri, 22 Dec 2023 08:23:42 +0100 Subject: [PATCH] Do not cache the output of `gitAskPass` Prior to this change, the `gitAskPass` program was called once on start-up and its output was cached and used for API calls to the forge while Git itself does not cache its output but calls it anytime a password is needed. This means if the output of `gitAskPass` changes during a Scala Steward run, the new password is only used for Git operations but not for forge API calls. With this change we now do the same as Git and call `gitAskPass` everytime the password is needed. This should make it easier to support GitHub Apps proper which require different access tokens during a run. See also https://github.com/scala-steward-org/scala-steward/issues/2973#issuecomment-1866224921. --- .../core/application/Config.scala | 20 -------- .../core/application/Context.scala | 5 +- .../core/forge/ForgeAuthAlg.scala | 46 +++++++++++++++++++ .../core/forge/ForgeSelection.scala | 26 ++++++----- .../azurerepos/AzureReposApiAlgTest.scala | 3 +- .../forge/bitbucket/BitbucketApiAlgTest.scala | 3 +- .../BitbucketServerApiAlgTest.scala | 5 +- .../core/forge/gitea/GiteaApiAlgTest.scala | 3 +- .../core/forge/github/GitHubApiAlgTest.scala | 3 +- .../core/forge/gitlab/GitLabApiAlgTest.scala | 13 +++--- 10 files changed, 81 insertions(+), 46 deletions(-) create mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala index 208ed68562..3385af7203 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala @@ -17,19 +17,13 @@ package org.scalasteward.core.application import better.files.File -import cats.Monad -import cats.syntax.all._ import org.http4s.Uri -import org.http4s.Uri.UserInfo import org.scalasteward.core.application.Cli.EnvVar import org.scalasteward.core.application.Config._ import org.scalasteward.core.data.Resolver import org.scalasteward.core.forge.ForgeType -import org.scalasteward.core.forge.data.AuthenticatedUser import org.scalasteward.core.forge.github.GitHubApp import org.scalasteward.core.git.Author -import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg} -import org.scalasteward.core.util import org.scalasteward.core.util.Nel import scala.concurrent.duration.FiniteDuration @@ -71,20 +65,6 @@ final case class Config( defaultResolver: Resolver, refreshBackoffPeriod: FiniteDuration ) { - def forgeUser[F[_]](implicit - processAlg: ProcessAlg[F], - workspaceAlg: WorkspaceAlg[F], - F: Monad[F] - ): F[AuthenticatedUser] = - for { - rootDir <- workspaceAlg.rootDir - urlWithUser = util.uri.withUserInfo - .replace(UserInfo(forgeCfg.login, None))(forgeCfg.apiHost) - .renderString - prompt = s"Password for '$urlWithUser': " - password <- processAlg.exec(Nel.of(gitCfg.gitAskPass.pathAsString, prompt), rootDir) - } yield AuthenticatedUser(forgeCfg.login, password.mkString.trim) - def forgeSpecificCfg: ForgeSpecificCfg = forgeCfg.tpe match { case ForgeType.AzureRepos => azureReposCfg diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala index 7ba009548b..75d009fc72 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala @@ -37,7 +37,7 @@ import org.scalasteward.core.edit.hooks.HookExecutor import org.scalasteward.core.edit.scalafix._ import org.scalasteward.core.edit.update.ScannerAlg import org.scalasteward.core.forge.github.{GitHubAppApiAlg, GitHubAuthAlg} -import org.scalasteward.core.forge.{ForgeApiAlg, ForgeRepoAlg, ForgeSelection} +import org.scalasteward.core.forge.{ForgeApiAlg, ForgeAuthAlg, ForgeRepoAlg, ForgeSelection} import org.scalasteward.core.git.{GenGitAlg, GitAlg} import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg} import org.scalasteward.core.nurture.{NurtureAlg, PullRequestRepository, UpdateInfoUrlFinder} @@ -130,7 +130,8 @@ object Context { F: Async[F] ): F[Context[F]] = for { - forgeUser <- config.forgeUser[F] + _ <- F.unit + forgeUser = new ForgeAuthAlg[F](config.gitCfg, config.forgeCfg).authenticatedUser artifactMigrationsLoader0 = new ArtifactMigrationsLoader[F] artifactMigrationsFinder0 <- artifactMigrationsLoader0.createFinder(config.artifactCfg) scalafixMigrationsLoader0 = new ScalafixMigrationsLoader[F] diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala new file mode 100644 index 0000000000..a7ca31b315 --- /dev/null +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala @@ -0,0 +1,46 @@ +/* + * Copyright 2018-2023 Scala Steward contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.scalasteward.core.forge + +import cats.Monad +import cats.syntax.all._ +import org.http4s.Uri.UserInfo +import org.scalasteward.core.application.Config.{ForgeCfg, GitCfg} +import org.scalasteward.core.forge.data.AuthenticatedUser +import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg} +import org.scalasteward.core.util +import org.scalasteward.core.util.Nel + +final class ForgeAuthAlg[F[_]](gitCfg: GitCfg, forgeCfg: ForgeCfg)(implicit + processAlg: ProcessAlg[F], + workspaceAlg: WorkspaceAlg[F], + F: Monad[F] +) { + def authenticatedUser: F[AuthenticatedUser] = + runAskPass.map(accessToken => AuthenticatedUser(forgeCfg.login, accessToken)) + + private def runAskPass: F[String] = + for { + rootDir <- workspaceAlg.rootDir + urlWithUser = util.uri.withUserInfo + .replace(UserInfo(forgeCfg.login, None))(forgeCfg.apiHost) + .renderString + prompt = s"Password for '$urlWithUser': " + output <- processAlg.exec(Nel.of(gitCfg.gitAskPass.pathAsString, prompt), rootDir) + password = output.mkString.trim + } yield password +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala index e53fe350b2..5dbe1c791d 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala @@ -18,7 +18,7 @@ package org.scalasteward.core.forge import cats.effect.Temporal import cats.syntax.all._ -import cats.{Applicative, Parallel} +import cats.{Applicative, Functor, Parallel} import org.http4s.headers.Authorization import org.http4s.{BasicCredentials, Header, Request} import org.scalasteward.core.application.Config @@ -39,7 +39,7 @@ object ForgeSelection { def forgeApiAlg[F[_]: Parallel]( forgeCfg: ForgeCfg, forgeSpecificCfg: ForgeSpecificCfg, - user: AuthenticatedUser + user: F[AuthenticatedUser] )(implicit httpJsonClient: HttpJsonClient[F], logger: Logger[F], @@ -64,15 +64,17 @@ object ForgeSelection { def authenticate[F[_]]( forgeType: ForgeType, - user: AuthenticatedUser - )(implicit F: Applicative[F]): Request[F] => F[Request[F]] = - forgeType match { - case AzureRepos => _.putHeaders(basicAuth(user)).pure[F] - case Bitbucket => _.putHeaders(basicAuth(user)).pure[F] - case BitbucketServer => _.putHeaders(basicAuth(user), xAtlassianToken).pure[F] - case GitHub => _.putHeaders(basicAuth(user)).pure[F] - case GitLab => _.putHeaders(Header.Raw(ci"Private-Token", user.accessToken)).pure[F] - case Gitea => _.putHeaders(basicAuth(user)).pure[F] + user: F[AuthenticatedUser] + )(implicit F: Functor[F]): Request[F] => F[Request[F]] = req => + user.map { user => + forgeType match { + case AzureRepos => req.putHeaders(basicAuth(user)) + case Bitbucket => req.putHeaders(basicAuth(user)) + case BitbucketServer => req.putHeaders(basicAuth(user), xAtlassianToken) + case GitHub => req.putHeaders(basicAuth(user)) + case GitLab => req.putHeaders(Header.Raw(ci"Private-Token", user.accessToken)) + case Gitea => req.putHeaders(basicAuth(user)) + } } private def basicAuth(user: AuthenticatedUser): Authorization = @@ -84,7 +86,7 @@ object ForgeSelection { def authenticateIfApiHost[F[_]]( forgeCfg: ForgeCfg, - user: AuthenticatedUser + user: F[AuthenticatedUser] )(implicit F: Applicative[F]): Request[F] => F[Request[F]] = req => { val sameScheme = req.uri.scheme === forgeCfg.apiHost.scheme diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala index 70b11e548b..c30d67fde7 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala @@ -18,6 +18,7 @@ import org.scalasteward.core.mock.{MockEff, MockState} class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { private val user = AuthenticatedUser("user", "pass") + private val userM = MockEff.pure(user) private val repo = Repo("scala-steward-org", "scala-steward") private val apiHost = uri"https://dev.azure.com" @@ -175,7 +176,7 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { private val forgeCfg = config.forgeCfg.copy(apiHost = apiHost, tpe = ForgeType.AzureRepos) private val azureReposCfg = AzureReposCfg(organization = Some("azure-org")) - private val azureReposApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, azureReposCfg, user) + private val azureReposApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, azureReposCfg, userM) test("getRepo") { val obtained = azureReposApiAlg.getRepo(repo).runA(state) diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala index be66fa5d47..4c06bc6824 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala @@ -21,6 +21,7 @@ import org.scalasteward.core.mock.{MockEff, MockState} class BitbucketApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { private val user = AuthenticatedUser("user", "pass") + private val userM = MockEff.pure(user) private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken)) private val auth = HttpApp[MockEff] { request => @@ -209,7 +210,7 @@ class BitbucketApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.Bitbucket) private val bitbucketCfg = BitbucketCfg(useDefaultReviewers = true) - private val bitbucketApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, bitbucketCfg, user) + private val bitbucketApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, bitbucketCfg, userM) private val prUrl = uri"https://bitbucket.org/fthomas/base.g8/pullrequests/2" private val repo = Repo("fthomas", "base.g8") diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala index 5b33d287a8..a8a6bf691e 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala @@ -22,6 +22,7 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] private val repo = Repo("scala-steward-org", "scala-steward") private val main = Branch("main") private val user = AuthenticatedUser("user", "pass") + private val userM = MockEff.pure(user) private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken)) private val auth = HttpApp[MockEff] { request => @@ -112,7 +113,7 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.BitbucketServer) private val bitbucketServerApiAlg = ForgeSelection - .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = false), user) + .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = false), userM) test("createPullRequest") { val data = NewPullRequestData( @@ -146,7 +147,7 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] reviewers = Nil ) val apiAlg = ForgeSelection - .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = true), user) + .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = true), userM) val pr = apiAlg.createPullRequest(repo, data).runA(state) val expected = PullRequestOut( diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala index 80f066bd0c..dcfa749814 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala @@ -20,6 +20,7 @@ import org.scalasteward.core.mock.{MockEff, MockState} class GiteaApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { private val user = AuthenticatedUser("user", "pass") + private val userM = MockEff.pure(user) private val repo = Repo("foo", "baz") private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken)) @@ -61,7 +62,7 @@ class GiteaApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { tpe = ForgeType.Gitea, apiHost = config.forgeCfg.apiHost / "api" / "v1" ) - private val giteaAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GiteaCfg(), user) + private val giteaAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GiteaCfg(), userM) test("getRepo") { giteaAlg diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala index e6f15da442..0c48e4a334 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala @@ -23,6 +23,7 @@ import org.scalasteward.core.mock.{MockEff, MockState} class GitHubApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { private val user = AuthenticatedUser("user", "pass") + private val userM = MockEff.pure(user) private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken)) private val auth = HttpApp[MockEff] { request => @@ -198,7 +199,7 @@ class GitHubApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { private val state = MockState.empty.copy(clientResponses = auth <+> httpApp) private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.GitHub) - private val gitHubApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GitHubCfg(), user) + private val gitHubApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GitHubCfg(), userM) private val repo = Repo("fthomas", "base.g8") diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala index b6a224456a..20c9fa128b 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala @@ -27,6 +27,7 @@ import org.typelevel.ci.CIStringSyntax class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { private val user = AuthenticatedUser("user", "pass") + private val userM = MockEff.pure(user) object MergeWhenPipelineSucceedsMatcher extends QueryParamDecoderMatcher[Boolean]("merge_when_pipeline_succeeds") @@ -127,7 +128,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = None, removeSourceBranch = false ), - user + userM ) private val gitlabApiAlgNoFork = ForgeSelection.forgeApiAlg[MockEff]( @@ -137,7 +138,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = None, removeSourceBranch = false ), - user + userM ) private val gitlabApiAlgAutoMerge = ForgeSelection.forgeApiAlg[MockEff]( @@ -147,7 +148,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = None, removeSourceBranch = false ), - user + userM ) private val gitlabApiAlgRemoveSourceBranch = ForgeSelection.forgeApiAlg[MockEff]( @@ -157,7 +158,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = None, removeSourceBranch = true ), - user + userM ) private val gitlabApiAlgLessReviewersRequired = ForgeSelection.forgeApiAlg[MockEff]( @@ -167,7 +168,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = Some(0), removeSourceBranch = false ), - user + userM ) private val gitlabApiAlgWithAssigneeAndReviewers = ForgeSelection.forgeApiAlg[MockEff]( @@ -177,7 +178,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] { requiredReviewers = Some(0), removeSourceBranch = false ), - user + userM ) private val data = UpdateData(