Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate jira plugin to Cats-effect #4281

Merged
merged 2 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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