Skip to content

Commit

Permalink
Migrate jira plugin to Cats-effect (#4281)
Browse files Browse the repository at this point in the history
Co-authored-by: Simon Dumas <[email protected]>
  • Loading branch information
imsdu and Simon Dumas authored Sep 21, 2023
1 parent 3e84402 commit 80f591b
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 133 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package ch.epfl.bluebrain.nexus.delta.plugins.jira

import akka.http.scaladsl.model.Uri
import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.plugins.jira.JiraError.{AccessTokenExpected, NoTokenError, RequestTokenExpected}
import ch.epfl.bluebrain.nexus.delta.plugins.jira.OAuthToken.{AccessToken, RequestToken}
import ch.epfl.bluebrain.nexus.delta.plugins.jira.config.JiraConfig
Expand All @@ -13,7 +15,6 @@ import com.google.api.client.http.{ByteArrayContent, GenericUrl}
import com.typesafe.scalalogging.Logger
import io.circe.JsonObject
import io.circe.syntax.EncoderOps
import monix.bio.{IO, Task}
import org.apache.commons.codec.binary.Base64

import java.nio.charset.StandardCharsets
Expand All @@ -29,47 +30,47 @@ trait JiraClient {
/**
* Creates an authorization request for the current user
*/
def requestToken()(implicit caller: User): IO[JiraError, AuthenticationRequest]
def requestToken()(implicit caller: User): IO[AuthenticationRequest]

/**
* Generates an access token for the current user by providing the verifier code provided by the user
*/
def accessToken(verifier: Verifier)(implicit caller: User): IO[JiraError, Unit]
def accessToken(verifier: Verifier)(implicit caller: User): IO[Unit]

/**
* Create an issue on behalf of the user in Jira
* @param payload
* the issue payload
*/
def createIssue(payload: JsonObject)(implicit caller: User): IO[JiraError, JiraResponse]
def createIssue(payload: JsonObject)(implicit caller: User): IO[JiraResponse]

/**
* Edits an issue on behalf of the user in Jira
* @param payload
* the issue payload
*/
def editIssue(issueId: String, payload: JsonObject)(implicit caller: User): IO[JiraError, JiraResponse]
def editIssue(issueId: String, payload: JsonObject)(implicit caller: User): IO[JiraResponse]

/**
* Get the issue matching the provided identifier
* @param issueId
* the identifier
*/
def getIssue(issueId: String)(implicit caller: User): IO[JiraError, JiraResponse]
def getIssue(issueId: String)(implicit caller: User): IO[JiraResponse]

/**
* List the projects the current user has access to
* @param recent
* when provided, return the n most recent projects the user was active in
*/
def listProjects(recent: Option[Int])(implicit caller: User): IO[JiraError, JiraResponse]
def listProjects(recent: Option[Int])(implicit caller: User): IO[JiraResponse]

/**
* Search issues in Jira the user has access to according to the provided search payload
* @param payload
* the search payload
*/
def search(payload: JsonObject)(implicit caller: User): IO[JiraError, JiraResponse]
def search(payload: JsonObject)(implicit caller: User): IO[JiraResponse]

}

Expand Down Expand Up @@ -101,34 +102,32 @@ object JiraClient {
* @param jiraConfig
* the jira configuration
*/
def apply(store: TokenStore, jiraConfig: JiraConfig): Task[JiraClient] = {
Task
.delay {
// Create the RSA signer according to the PKCS8 key provided by the configuration
val privateBytes = Base64.decodeBase64(jiraConfig.privateKey.value)
val keySpec = new PKCS8EncodedKeySpec(privateBytes)
val kf = KeyFactory.getInstance("RSA")
val signer = new OAuthRsaSigner()
signer.privateKey = kf.generatePrivate(keySpec)
signer
}
def apply(store: TokenStore, jiraConfig: JiraConfig): IO[JiraClient] = {
IO {
// Create the RSA signer according to the PKCS8 key provided by the configuration
val privateBytes = Base64.decodeBase64(jiraConfig.privateKey.value)
val keySpec = new PKCS8EncodedKeySpec(privateBytes)
val kf = KeyFactory.getInstance("RSA")
val signer = new OAuthRsaSigner()
signer.privateKey = kf.generatePrivate(keySpec)
signer
}
.map { signer =>
new JiraClient {

private val netHttpTransport = new NetHttpTransport()

override def requestToken()(implicit caller: User): IO[JiraError, AuthenticationRequest] =
Task
.delay {
val tempToken = new JiraOAuthGetTemporaryToken(jiraConfig.base)
tempToken.consumerKey = jiraConfig.consumerKey
tempToken.signer = signer
tempToken.transport = netHttpTransport
tempToken.callback = "oob"
val response = tempToken.execute()
logger.debug(s"Request Token value: ${response.token}")
response.token
}
override def requestToken()(implicit caller: User): IO[AuthenticationRequest] =
IO {
val tempToken = new JiraOAuthGetTemporaryToken(jiraConfig.base)
tempToken.consumerKey = jiraConfig.consumerKey
tempToken.signer = signer
tempToken.transport = netHttpTransport
tempToken.callback = "oob"
val response = tempToken.execute()
logger.debug(s"Request Token value: ${response.token}")
response.token
}
.flatMap { token =>
store.save(caller, RequestToken(token)).as {
val authorizationURL =
Expand All @@ -137,33 +136,32 @@ object JiraClient {
AuthenticationRequest(Uri(authorizationURL.toString))
}
}
.mapError { JiraError.from }
.adaptError { e => JiraError.from(e) }

override def accessToken(verifier: Verifier)(implicit caller: User): IO[JiraError, Unit] =
override def accessToken(verifier: Verifier)(implicit caller: User): IO[Unit] =
store
.get(caller)
.flatMap {
case None => IO.raiseError(NoTokenError)
case Some(_: AccessToken) => IO.raiseError(RequestTokenExpected)
case Some(RequestToken(value)) =>
Task
.delay {
val accessToken = new JiraOAuthGetAccessToken(jiraConfig.base)
accessToken.consumerKey = jiraConfig.consumerKey
accessToken.signer = signer
accessToken.transport = netHttpTransport
accessToken.verifier = verifier.value
accessToken.temporaryToken = value
accessToken.execute().token
}
IO {
val accessToken = new JiraOAuthGetAccessToken(jiraConfig.base)
accessToken.consumerKey = jiraConfig.consumerKey
accessToken.signer = signer
accessToken.transport = netHttpTransport
accessToken.verifier = verifier.value
accessToken.temporaryToken = value
accessToken.execute().token
}
.flatMap { token =>
logger.debug("Access Token:" + token)
store.save(caller, AccessToken(token))
}
}
.mapError { JiraError.from }
.adaptError { e => JiraError.from(e) }

override def createIssue(payload: JsonObject)(implicit caller: User): IO[JiraError, JiraResponse] =
override def createIssue(payload: JsonObject)(implicit caller: User): IO[JiraResponse] =
requestFactory(caller).flatMap { factory =>
val url = jiraConfig.base / issueUrl
JiraResponse(
Expand All @@ -176,7 +174,7 @@ object JiraClient {

override def editIssue(issueId: String, payload: JsonObject)(implicit
caller: User
): IO[JiraError, JiraResponse] =
): IO[JiraResponse] =
requestFactory(caller).flatMap { factory =>
val url = jiraConfig.base / issueUrl / issueId
JiraResponse(
Expand All @@ -187,7 +185,7 @@ object JiraClient {
)
}

override def getIssue(issueId: String)(implicit caller: User): IO[JiraError, JiraResponse] =
override def getIssue(issueId: String)(implicit caller: User): IO[JiraResponse] =
requestFactory(caller).flatMap { factory =>
val url = jiraConfig.base / issueUrl / issueId
JiraResponse(
Expand All @@ -197,7 +195,7 @@ object JiraClient {
)
}

override def listProjects(recent: Option[Int])(implicit caller: User): IO[JiraError, JiraResponse] =
override def listProjects(recent: Option[Int])(implicit caller: User): IO[JiraResponse] =
requestFactory(caller).flatMap { factory =>
val url = recent.fold(jiraConfig.base / projectUrl) { r =>
(jiraConfig.base / projectUrl).withQuery(Uri.Query("recent" -> r.toString))
Expand All @@ -209,7 +207,7 @@ object JiraClient {
)
}

def search(payload: JsonObject)(implicit caller: User): IO[JiraError, JiraResponse] =
def search(payload: JsonObject)(implicit caller: User): IO[JiraResponse] =
requestFactory(caller).flatMap { factory =>
JiraResponse(
factory.buildPostRequest(
Expand All @@ -219,7 +217,7 @@ object JiraClient {
)
}

private def requestFactory(caller: User) = store.get(caller).hideErrors.flatMap {
private def requestFactory(caller: User) = store.get(caller).flatMap {
case None => IO.raiseError(NoTokenError)
case Some(_: RequestToken) => IO.raiseError(AccessTokenExpected)
case Some(AccessToken(token)) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package ch.epfl.bluebrain.nexus.delta.plugins.jira

import cats.effect.Clock
import cats.effect.{Clock, IO}
import ch.epfl.bluebrain.nexus.delta.plugins.jira.config.JiraConfig
import ch.epfl.bluebrain.nexus.delta.plugins.jira.routes.JiraRoutes
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
Expand All @@ -11,7 +11,6 @@ import ch.epfl.bluebrain.nexus.delta.sdk.identities.Identities
import ch.epfl.bluebrain.nexus.delta.sdk.model._
import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors
import izumi.distage.model.definition.{Id, ModuleDef}
import monix.bio.UIO
import monix.execution.Scheduler

/**
Expand All @@ -21,7 +20,7 @@ class JiraPluginModule(priority: Int) extends ModuleDef {

make[JiraConfig].from { JiraConfig.load(_) }

make[JiraClient].fromEffect { (xas: Transactors, jiraConfig: JiraConfig, clock: Clock[UIO]) =>
make[JiraClient].fromEffect { (xas: Transactors, jiraConfig: JiraConfig, clock: Clock[IO]) =>
JiraClient(TokenStore(xas)(clock), jiraConfig)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package ch.epfl.bluebrain.nexus.delta.plugins.jira

import cats.effect.Clock
import ch.epfl.bluebrain.nexus.delta.kernel.utils.IOUtils.instant
import cats.effect.{Clock, IO}
import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._
import ch.epfl.bluebrain.nexus.delta.kernel.utils.IOInstant
import ch.epfl.bluebrain.nexus.delta.sourcing.Transactors
import ch.epfl.bluebrain.nexus.delta.sourcing.implicits._
import ch.epfl.bluebrain.nexus.delta.sourcing.model.Identity
Expand All @@ -10,7 +11,6 @@ import doobie.implicits._
import doobie.postgres.implicits._
import io.circe.Json
import io.circe.syntax._
import monix.bio.{Task, UIO}

/**
* Stores Jira tokens in the underlying databases
Expand All @@ -22,7 +22,7 @@ trait TokenStore {
* @param user
* the user
*/
def get(user: User): Task[Option[OAuthToken]]
def get(user: User): IO[Option[OAuthToken]]

/**
* Save the token for the given user
Expand All @@ -31,7 +31,7 @@ trait TokenStore {
* @param oauthToken
* the associated token
*/
def save(user: User, oauthToken: OAuthToken): Task[Unit]
def save(user: User, oauthToken: OAuthToken): IO[Unit]

}

Expand All @@ -40,21 +40,23 @@ object TokenStore {
/**
* Create a token store
*/
def apply(xas: Transactors)(implicit clock: Clock[UIO]): TokenStore = {
def apply(xas: Transactors)(implicit clock: Clock[IO]): TokenStore = {
new TokenStore {
override def get(user: Identity.User): Task[Option[OAuthToken]] =
sql"SELECT token_value FROM jira_tokens WHERE realm = ${user.realm.value} and subject = ${user.subject}"
.query[Json]
.option
.transact(xas.read)
override def get(user: Identity.User): IO[Option[OAuthToken]] =
toCatsIO(
sql"SELECT token_value FROM jira_tokens WHERE realm = ${user.realm.value} and subject = ${user.subject}"
.query[Json]
.option
.transact(xas.read)
)
.flatMap {
case Some(token) =>
Task.fromEither(token.as[OAuthToken]).map(Some(_))
case None => Task.none
IO.fromEither(token.as[OAuthToken]).map(Some(_))
case None => IO.none
}

override def save(user: Identity.User, oauthToken: OAuthToken): Task[Unit] =
instant.flatMap { now =>
override def save(user: Identity.User, oauthToken: OAuthToken): IO[Unit] =
IOInstant.now.flatMap { now =>
sql""" INSERT INTO jira_tokens(realm, subject, instant, token_value)
| VALUES(${user.realm.value}, ${user.subject}, $now, ${oauthToken.asJson})
| ON CONFLICT (realm, subject) DO UPDATE SET instant = EXCLUDED.instant, token_value = EXCLUDED.token_value
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package ch.epfl.bluebrain.nexus.delta.plugins.jira.model

import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.plugins.jira.JiraError
import com.google.api.client.http.HttpRequest
import io.circe.{parser, Json}
import monix.bio.{IO, Task}

/**
* Jira response
Expand All @@ -12,19 +13,16 @@ final case class JiraResponse(content: Option[Json])

object JiraResponse {

def apply(request: HttpRequest): IO[JiraError, JiraResponse] = {
Task
.delay(
request.execute()
)
def apply(request: HttpRequest): IO[JiraResponse] = {
IO(request.execute())
.flatMap { response =>
val content = response.parseAsString()
if (content.nonEmpty) {
Task.fromEither(parser.parse(content)).map { r => JiraResponse(Some(r)) }
IO.fromEither(parser.parse(content)).map { r => JiraResponse(Some(r)) }
} else {
Task.pure(JiraResponse(None))
IO.pure(JiraResponse(None))
}
}
.mapError { JiraError.from }
.adaptError { e => JiraError.from(e) }
}
}
Loading

0 comments on commit 80f591b

Please sign in to comment.