Skip to content

Commit

Permalink
Do not cache the output of gitAskPass
Browse files Browse the repository at this point in the history
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 #2973 (comment).
  • Loading branch information
fthomas committed Dec 22, 2023
1 parent a3a2507 commit 12a1410
Show file tree
Hide file tree
Showing 10 changed files with 81 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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],
Expand All @@ -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 =
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -127,7 +128,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
requiredReviewers = None,
removeSourceBranch = false
),
user
userM
)

private val gitlabApiAlgNoFork = ForgeSelection.forgeApiAlg[MockEff](
Expand All @@ -137,7 +138,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
requiredReviewers = None,
removeSourceBranch = false
),
user
userM
)

private val gitlabApiAlgAutoMerge = ForgeSelection.forgeApiAlg[MockEff](
Expand All @@ -147,7 +148,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
requiredReviewers = None,
removeSourceBranch = false
),
user
userM
)

private val gitlabApiAlgRemoveSourceBranch = ForgeSelection.forgeApiAlg[MockEff](
Expand All @@ -157,7 +158,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
requiredReviewers = None,
removeSourceBranch = true
),
user
userM
)

private val gitlabApiAlgLessReviewersRequired = ForgeSelection.forgeApiAlg[MockEff](
Expand All @@ -167,7 +168,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
requiredReviewers = Some(0),
removeSourceBranch = false
),
user
userM
)

private val gitlabApiAlgWithAssigneeAndReviewers = ForgeSelection.forgeApiAlg[MockEff](
Expand All @@ -177,7 +178,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
requiredReviewers = Some(0),
removeSourceBranch = false
),
user
userM
)

private val data = UpdateData(
Expand Down

0 comments on commit 12a1410

Please sign in to comment.