From 43f9fd54e073005d0009b0e55fcbc845bf4069b9 Mon Sep 17 00:00:00 2001 From: Robertas Zamblauskas Date: Wed, 25 Oct 2017 13:14:31 +0300 Subject: [PATCH 1/6] Refactored request authentication to a separate object. Added basic auth. --- .../bitbucket/client/Authentication.scala | 44 +++++++++++++++++++ .../bitbucket/client/BitbucketClient.scala | 17 ++++--- 2 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 src/main/scala/com/codacy/client/bitbucket/client/Authentication.scala diff --git a/src/main/scala/com/codacy/client/bitbucket/client/Authentication.scala b/src/main/scala/com/codacy/client/bitbucket/client/Authentication.scala new file mode 100644 index 0000000..e7ca36f --- /dev/null +++ b/src/main/scala/com/codacy/client/bitbucket/client/Authentication.scala @@ -0,0 +1,44 @@ +package com.codacy.client.bitbucket.client + +import play.api.libs.oauth.{ConsumerKey, OAuthCalculator, RequestToken} +import play.api.libs.ws.{WSAuthScheme, WSRequest} + +object Authentication { + + sealed trait Credentials + + case class OAuthCredentials(key: String, secretKey: String, token: String, secretToken: String) extends Credentials + + case class BasicAuthCredentials(username: String, password: String) extends Credentials + + + sealed trait Authenticator { + def authenticate(req: WSRequest): WSRequest + } + + object Authenticator { + def fromCredentials(credentials: Credentials): Authenticator = { + credentials match { + case c: OAuthCredentials => new OAuthAuthenticator(c) + case c: BasicAuthCredentials => new BasicAuthAuthenticator(c) + } + } + } + + class OAuthAuthenticator(credentials: OAuthCredentials) extends Authenticator { + private lazy val KEY = ConsumerKey(credentials.key, credentials.secretKey) + private lazy val TOKEN = RequestToken(credentials.token, credentials.secretToken) + + private lazy val requestSigner = OAuthCalculator(KEY, TOKEN) + + def authenticate(req: WSRequest): WSRequest = req.sign(requestSigner) + } + + class BasicAuthAuthenticator(credentials: BasicAuthCredentials) extends Authenticator { + def authenticate(req: WSRequest): WSRequest = req.withAuth(credentials.username, credentials.password, WSAuthScheme.BASIC) + } + + implicit class WsRequestExtensions(val req: WSRequest) extends AnyVal { + def authenticate(authenticator: Authenticator): WSRequest = authenticator.authenticate(req) + } +} diff --git a/src/main/scala/com/codacy/client/bitbucket/client/BitbucketClient.scala b/src/main/scala/com/codacy/client/bitbucket/client/BitbucketClient.scala index 43773d0..9a40a27 100644 --- a/src/main/scala/com/codacy/client/bitbucket/client/BitbucketClient.scala +++ b/src/main/scala/com/codacy/client/bitbucket/client/BitbucketClient.scala @@ -1,27 +1,26 @@ package com.codacy.client.bitbucket.client import java.net.URI -import java.util.concurrent.{SynchronousQueue, ThreadPoolExecutor, TimeUnit} +import com.codacy.client.bitbucket.client.Authentication._ import com.codacy.client.bitbucket.util.HTTPStatusCodes import com.codacy.client.bitbucket.util.Implicits.URIQueryParam import com.ning.http.client.AsyncHttpClientConfig import play.api.http.{ContentTypeOf, Writeable} import play.api.libs.json._ -import play.api.libs.oauth._ import play.api.libs.ws.ning.{NingAsyncHttpClientConfigBuilder, NingWSClient} import scala.concurrent.Await import scala.concurrent.duration.{Duration, SECONDS} import scala.util.{Failure, Properties, Success, Try} -class BitbucketClient(key: String, secretKey: String, token: String, secretToken: String) { - private lazy val KEY = ConsumerKey(key, secretKey) - private lazy val TOKEN = RequestToken(token, secretToken) + +class BitbucketClient(credentials: Credentials) { private lazy val requestTimeout = Duration(10, SECONDS) - private lazy val requestSigner = OAuthCalculator(KEY, TOKEN) + + private lazy val authenticator = Authenticator.fromCredentials(credentials) /* * Does an API request and parses the json output into a class @@ -75,7 +74,7 @@ class BitbucketClient(key: String, secretKey: String, token: String, secretToken */ private def performRequest[D, T](method: String, request: Request[T], values: D)(implicit reader: Reads[T], writer: Writeable[D], contentType: ContentTypeOf[D]): RequestResponse[T] = withClientRequest { client => val jpromise = client.url(request.url) - .sign(requestSigner) + .authenticate(authenticator) .withFollowRedirects(follow = true) .withMethod(method).withBody(values).execute() val result = Await.result(jpromise, requestTimeout) @@ -128,7 +127,7 @@ class BitbucketClient(key: String, secretKey: String, token: String, secretToken /* copy paste from post ... */ def delete[T](url: String): RequestResponse[Boolean] = withClientRequest { client => val jpromise = client.url(url) - .sign(requestSigner) + .authenticate(authenticator) .withFollowRedirects(follow = true) .delete() val result = Await.result(jpromise, requestTimeout) @@ -144,7 +143,7 @@ class BitbucketClient(key: String, secretKey: String, token: String, secretToken private def get(url: String): Either[ResponseError, JsValue] = withClientEither { client => val jpromise = client.url(url) - .sign(requestSigner) + .authenticate(authenticator) .withFollowRedirects(follow = true) .get() val result = Await.result(jpromise, requestTimeout) From efaca2ce8e6f46ec31d85157196f86a9cae009a9 Mon Sep 17 00:00:00 2001 From: Robertas Zamblauskas Date: Wed, 25 Oct 2017 13:15:29 +0300 Subject: [PATCH 2/6] Added `HookServices#update`. --- .../client/bitbucket/service/HookServices.scala | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/scala/com/codacy/client/bitbucket/service/HookServices.scala b/src/main/scala/com/codacy/client/bitbucket/service/HookServices.scala index e1827c9..7c0526d 100644 --- a/src/main/scala/com/codacy/client/bitbucket/service/HookServices.scala +++ b/src/main/scala/com/codacy/client/bitbucket/service/HookServices.scala @@ -19,10 +19,21 @@ class HookServices(client: BitbucketClient) { "url" -> hookUrl, "events" -> events ) - client.postJson(Request(servicesUrl, classOf[Webhook]), payload) } + def update(author: String, repo: String, uuid: String, + active: Boolean, description: String, hookUrl: String, events:Set[String]): RequestResponse[Webhook] = { + val servicesUrl = getServicesUrl(author, repo) + val payload = Json.obj( + "active" -> active, + "description" -> description, + "url" -> hookUrl, + "events" -> events + ) + client.putJson(Request(s"$servicesUrl/$uuid", classOf[Webhook]), payload) + } + def delete(author: String, repo: String, uuid: String): RequestResponse[Boolean] = { val servicesUrl = getServicesUrl(author, repo) client.delete(s"$servicesUrl/$uuid") From cef16bb7dd908b97abddceda490965d187ede0ea Mon Sep 17 00:00:00 2001 From: Robertas Zamblauskas Date: Wed, 25 Oct 2017 13:51:17 +0300 Subject: [PATCH 3/6] A bit of scaladoc on `Authentication`. --- .../client/bitbucket/client/Authentication.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/scala/com/codacy/client/bitbucket/client/Authentication.scala b/src/main/scala/com/codacy/client/bitbucket/client/Authentication.scala index e7ca36f..0a8644a 100644 --- a/src/main/scala/com/codacy/client/bitbucket/client/Authentication.scala +++ b/src/main/scala/com/codacy/client/bitbucket/client/Authentication.scala @@ -3,12 +3,21 @@ package com.codacy.client.bitbucket.client import play.api.libs.oauth.{ConsumerKey, OAuthCalculator, RequestToken} import play.api.libs.ws.{WSAuthScheme, WSRequest} +/** + * Handles request authentication. + * Provides several different authentication options. + * + * @author - Robertas Zamblauskas + */ object Authentication { sealed trait Credentials case class OAuthCredentials(key: String, secretKey: String, token: String, secretToken: String) extends Credentials + /** + * Your username and password | app password. + */ case class BasicAuthCredentials(username: String, password: String) extends Credentials @@ -38,6 +47,9 @@ object Authentication { def authenticate(req: WSRequest): WSRequest = req.withAuth(credentials.username, credentials.password, WSAuthScheme.BASIC) } + /** + * Provide nicer syntax for authentication. + */ implicit class WsRequestExtensions(val req: WSRequest) extends AnyVal { def authenticate(authenticator: Authenticator): WSRequest = authenticator.authenticate(req) } From 2ed4d572c0d641edfd82f44cac5c1b5f685cedf9 Mon Sep 17 00:00:00 2001 From: Robertas Zamblauskas Date: Wed, 25 Oct 2017 13:56:44 +0300 Subject: [PATCH 4/6] Removed '!' from all API urls - I've no idea why it was there, but it must be removed for basic auth to work. --- .../bitbucket/service/BuildStatusServices.scala | 6 +++--- .../bitbucket/service/CommitServices.scala | 4 ++-- .../bitbucket/service/PullRequestServices.scala | 16 ++++++++-------- .../bitbucket/service/RepositoryServices.scala | 6 +++--- .../client/bitbucket/service/UserServices.scala | 6 +++--- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/scala/com/codacy/client/bitbucket/service/BuildStatusServices.scala b/src/main/scala/com/codacy/client/bitbucket/service/BuildStatusServices.scala index 49f28da..80a2603 100644 --- a/src/main/scala/com/codacy/client/bitbucket/service/BuildStatusServices.scala +++ b/src/main/scala/com/codacy/client/bitbucket/service/BuildStatusServices.scala @@ -11,7 +11,7 @@ class BuildStatusServices(client: BitbucketClient) { * */ def getBuildStatus(owner: String, repository: String, commit: String, key: String): RequestResponse[BuildStatus] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/commit/$commit/statuses/build/$key" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/commit/$commit/statuses/build/$key" client.execute(Request(url, classOf[BuildStatus])) } @@ -22,7 +22,7 @@ class BuildStatusServices(client: BitbucketClient) { * */ def createBuildStatus(owner: String, repository: String, commit: String, buildStatus: BuildStatus): RequestResponse[BuildStatus] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/commit/$commit/statuses/build" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/commit/$commit/statuses/build" val values = Map("state" -> Seq(buildStatus.state.toString), "key" -> Seq(buildStatus.key), "name" -> Seq(buildStatus.name), "url" -> Seq(buildStatus.url), @@ -36,7 +36,7 @@ class BuildStatusServices(client: BitbucketClient) { * */ def updateBuildStatus(owner: String, repository: String, commit: String, buildStatus: BuildStatus): RequestResponse[BuildStatus] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/commit/$commit/statuses/build/${buildStatus.key}" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/commit/$commit/statuses/build/${buildStatus.key}" val payload = Json.obj( "state" -> buildStatus.state, diff --git a/src/main/scala/com/codacy/client/bitbucket/service/CommitServices.scala b/src/main/scala/com/codacy/client/bitbucket/service/CommitServices.scala index 2c5ea5f..ffd1047 100644 --- a/src/main/scala/com/codacy/client/bitbucket/service/CommitServices.scala +++ b/src/main/scala/com/codacy/client/bitbucket/service/CommitServices.scala @@ -8,7 +8,7 @@ import play.api.libs.json.{JsNumber, JsObject, JsString} class CommitServices(client: BitbucketClient) { def createComment(author: String, repo: String, commit: String, body: String, file: Option[String] = None, line: Option[Int] = None): RequestResponse[CommitComment] = { - val url = s"https://bitbucket.org/!api/1.0/repositories/$author/$repo/changesets/${CommitHelper.anchor(commit)}/comments" + val url = s"https://bitbucket.org/api/1.0/repositories/$author/$repo/changesets/${CommitHelper.anchor(commit)}/comments" val params = file.map(filename => "filename" -> JsString(filename)) ++ line.map(lineTo => "line_to" -> JsNumber(lineTo)) @@ -19,7 +19,7 @@ class CommitServices(client: BitbucketClient) { } def deleteComment(author: String, repo: String, commit: String, commentId: Long): Unit = { - val url = s"https://bitbucket.org/!api/1.0/repositories/$author/$repo/changesets/${CommitHelper.anchor(commit)}/comments/$commentId" + val url = s"https://bitbucket.org/api/1.0/repositories/$author/$repo/changesets/${CommitHelper.anchor(commit)}/comments/$commentId" client.delete(url) } diff --git a/src/main/scala/com/codacy/client/bitbucket/service/PullRequestServices.scala b/src/main/scala/com/codacy/client/bitbucket/service/PullRequestServices.scala index 1a00345..a5431e9 100644 --- a/src/main/scala/com/codacy/client/bitbucket/service/PullRequestServices.scala +++ b/src/main/scala/com/codacy/client/bitbucket/service/PullRequestServices.scala @@ -14,7 +14,7 @@ class PullRequestServices(client: BitbucketClient) { * */ def getPullRequests(owner: String, repository: String, states: Seq[String] = Seq("OPEN")): RequestResponse[Seq[PullRequest]] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/pullrequests?pagelen=50&state=${states.mkString("&state=")}" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/pullrequests?pagelen=50&state=${states.mkString("&state=")}" client.executePaginated(Request(url, classOf[Seq[PullRequest]])) } @@ -24,13 +24,13 @@ class PullRequestServices(client: BitbucketClient) { * */ def getPullRequestCommits(owner: String, repository: String, prId: Long): RequestResponse[Seq[SimpleCommit]] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/pullrequests/$prId/commits?pagelen=100" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/pullrequests/$prId/commits?pagelen=100" client.executePaginated(Request(url, classOf[Seq[SimpleCommit]])) } def create(owner: String, repository: String, title: String, sourceBranch: String, destinationBranch: String): RequestResponse[JsObject] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/pullrequests" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/pullrequests" val payload = Json.obj( "title" -> title, @@ -50,22 +50,22 @@ class PullRequestServices(client: BitbucketClient) { } def postApprove(owner: String, repository: String, prId: Long): RequestResponse[JsObject] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/pullrequests/$prId/approve" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/pullrequests/$prId/approve" client.postJson(Request(url, classOf[JsObject]), JsNull) } def deleteApprove(owner: String, repository: String, prId: Long): RequestResponse[Boolean] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/pullrequests/$prId/approve" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/pullrequests/$prId/approve" client.delete(url) } def merge(owner: String, repository: String, prId: Long): RequestResponse[JsObject] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/pullrequests/$prId/merge" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/pullrequests/$prId/merge" client.postJson(Request(url, classOf[JsObject]), JsNull) } def decline(owner: String, repository: String, prId: Long): RequestResponse[JsObject] = { - val url = s"https://bitbucket.org/!api/2.0/repositories/$owner/$repository/pullrequests/$prId/decline" + val url = s"https://bitbucket.org/api/2.0/repositories/$owner/$repository/pullrequests/$prId/decline" client.postJson(Request(url, classOf[JsObject]), JsNull) } @@ -95,7 +95,7 @@ class PullRequestServices(client: BitbucketClient) { } def listComments(author: String, repo: String, pullRequestId: Int): RequestResponse[Seq[SimplePullRequestComment]] = { - val url = s"https://bitbucket.org/!api/1.0/repositories/$author/$repo/pullrequests/$pullRequestId/comments" + val url = s"https://bitbucket.org/api/1.0/repositories/$author/$repo/pullrequests/$pullRequestId/comments" client.execute(Request(url, classOf[Seq[SimplePullRequestComment]])) } diff --git a/src/main/scala/com/codacy/client/bitbucket/service/RepositoryServices.scala b/src/main/scala/com/codacy/client/bitbucket/service/RepositoryServices.scala index 352d2dc..bd3c680 100644 --- a/src/main/scala/com/codacy/client/bitbucket/service/RepositoryServices.scala +++ b/src/main/scala/com/codacy/client/bitbucket/service/RepositoryServices.scala @@ -11,7 +11,7 @@ class RepositoryServices(client: BitbucketClient) { * Use this if you're looking for a full list of all of the repositories associated with a user */ def getRepositories: RequestResponse[Seq[SimpleRepository]] = { - client.execute(Request(s"https://bitbucket.org/!api/1.0/user/repositories", classOf[Seq[SimpleRepository]])) + client.execute(Request(s"https://bitbucket.org/api/1.0/user/repositories", classOf[Seq[SimpleRepository]])) } /* @@ -19,14 +19,14 @@ class RepositoryServices(client: BitbucketClient) { * if the caller is authenticated and is authorized to view the repository. */ def getRepositories(username: String): RequestResponse[Seq[Repository]] = { - client.executePaginated(Request(s"https://bitbucket.org/!api/2.0/repositories/$username", classOf[Seq[Repository]])) + client.executePaginated(Request(s"https://bitbucket.org/api/2.0/repositories/$username", classOf[Seq[Repository]])) } /* * Creates a ssh key */ def createKey(username: String, repo: String, key: String): RequestResponse[SshKey] = { - val url = s"https://bitbucket.org/!api/1.0/repositories/$username/$repo/deploy-keys" + val url = s"https://bitbucket.org/api/1.0/repositories/$username/$repo/deploy-keys" val values = Json.obj( "key" -> key, diff --git a/src/main/scala/com/codacy/client/bitbucket/service/UserServices.scala b/src/main/scala/com/codacy/client/bitbucket/service/UserServices.scala index bb9e00b..6b304c4 100644 --- a/src/main/scala/com/codacy/client/bitbucket/service/UserServices.scala +++ b/src/main/scala/com/codacy/client/bitbucket/service/UserServices.scala @@ -10,21 +10,21 @@ class UserServices(client: BitbucketClient) { * Gets the basic information associated with the token owner account. */ def getUser: RequestResponse[User] = { - client.execute(Request("https://bitbucket.org/!api/1.0/user", classOf[User])) + client.execute(Request("https://bitbucket.org/api/1.0/user", classOf[User])) } /* * Gets the basic information associated with an account. */ def getUser(username: String): RequestResponse[User] = { - client.execute(Request(s"https://bitbucket.org/!api/1.0/users/$username", classOf[User])) + client.execute(Request(s"https://bitbucket.org/api/1.0/users/$username", classOf[User])) } /* * Creates a ssh key */ def createKey(username: String, key: String): RequestResponse[SshKey] = { - val url = s"https://bitbucket.org/!api/1.0/users/$username/ssh-keys" + val url = s"https://bitbucket.org/api/1.0/users/$username/ssh-keys" val values = Json.obj( "key" -> key, From c53f482e731e8aeaccc140579cc31525f9b3d132 Mon Sep 17 00:00:00 2001 From: Robertas Zamblauskas Date: Wed, 25 Oct 2017 13:59:48 +0300 Subject: [PATCH 5/6] Updated project version. --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index a96b2b9..62523f8 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import Dependencies._ name := """bitbucket-scala-client""" -version := "1.7.1-SNAPSHOT" +version := "1.7.1-zamblauskas-bulk-hook-update-SNAPSHOT" scalaVersion := "2.10.5" From 37baffad48d0c82de4f2e8bb7dc1e5575f4976fd Mon Sep 17 00:00:00 2001 From: Robertas Zamblauskas Date: Mon, 30 Oct 2017 19:31:21 +0200 Subject: [PATCH 6/6] Preparing for merge - bumped version. --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 62523f8..6e5b1c5 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import Dependencies._ name := """bitbucket-scala-client""" -version := "1.7.1-zamblauskas-bulk-hook-update-SNAPSHOT" +version := "1.8.0-SNAPSHOT" scalaVersion := "2.10.5"