Skip to content

Commit

Permalink
Merge pull request #27 from Ferlab-Ste-Justine/feat/cqdg-752_753_miss…
Browse files Browse the repository at this point in the history
…ing_files_permissions_and_refresh_token

feat/CQDG-752 and CQDG-753 display unauthorized documents / refresh token
  • Loading branch information
adipaul1981 authored May 27, 2024
2 parents a54ba3d + f2e8dd4 commit 8130718
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/main/scala/ca/ferlab/ferload/client/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import picocli.CommandLine.Command
import java.io.File

@Command(name = "ferload-client", mixinStandardHelpOptions = true,
version = Array("2.0.0"),
version = Array("2.1.0"),
description = Array("Official Ferload Client command line interface for files download."),
subcommands = Array(classOf[Configure], classOf[Download]))
class Main extends Runnable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import org.apache.http.util.EntityUtils
import org.json.JSONObject

import scala.jdk.CollectionConverters.MapHasAsScala // scala 2.13
// import scala.collection.JavaConverters._ // scala 2.12

abstract class BaseHttpClient {

Expand All @@ -27,7 +26,9 @@ abstract class BaseHttpClient {
}

protected def formatExceptionMessage(message: String, status: Int, body: Option[String]): String = {
s"$message, code: $status, message:\n${body.getOrElse("")}"
val msg = body.map(r => new JSONObject(r).get("msg"))

s"$message, code: $status, message:\n${msg.getOrElse("")}"
}

protected def toMap(body: Option[String], lineContents: Seq[LineContent]): Map[LineContent, String] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class FerloadClient(userConfig: UserConfig) extends BaseHttpClient with IFerload
val (body, status) = executeHttpRequest(httpRequest)
status match {
case 200 => toMap(body, manifestContent.lines)
case 403 => throw new IllegalStateException(formatExceptionMessage("No enough access rights to download the following files", status, body))
case 403 => throw new UnauthorizedException(formatExceptionMessage("No enough access rights to download the following files", status, body))
case _ => throw new IllegalStateException(formatExceptionMessage("Failed to retrieve download link(s)", status, body))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class KeycloakClient(config: UserConfig) extends BaseHttpClient with IKeycloak {
if (StringUtils.isNoneBlank(username, password)) {
getRPT(getAccessToken(username, password))
} else if(StringUtils.isNotBlank(refreshToken)) {
getRefreshedToken(refreshToken)
getRefreshedToken(refreshToken)(realm = config.get(TokenRealm), client = config.get(TokenClientId))
} else {
throw new IllegalStateException("No valid user credentials")
}
Expand All @@ -49,7 +49,7 @@ class KeycloakClient(config: UserConfig) extends BaseHttpClient with IKeycloak {
executeDeviceFetch(request)
}

override def getUserDeviceToken(deviceCode: String, expiresIn: Int = MAX_TOKEN_EXPIRE): String = {
override def getUserDeviceToken(deviceCode: String, expiresIn: Int = MAX_TOKEN_EXPIRE): (String, String) = {
val request = new HttpPost(s"${config.get(KeycloakUrl)}/realms/${config.get(KeycloakRealm)}/protocol/openid-connect/token")
request.addHeader("Content-Type", "application/x-www-form-urlencoded")

Expand All @@ -63,6 +63,12 @@ class KeycloakClient(config: UserConfig) extends BaseHttpClient with IKeycloak {
executeWithRetry(request, expiresIn)
}

override def getRefreshedTokens(refreshToken: String)(realm: String, client: String): (String, String) = {
val resp = refreshCredentials(refreshToken: String)(realm: String, client: String)

(resp.getString("access_token"), resp.getString("refresh_token"))
}

private def getAccessToken(username: String, password: String) = {
val request = new HttpPost(s"${config.get(KeycloakUrl)}/realms/${config.get(KeycloakRealm)}/protocol/openid-connect/token")

Expand All @@ -74,14 +80,20 @@ class KeycloakClient(config: UserConfig) extends BaseHttpClient with IKeycloak {

request.setEntity(new UrlEncodedFormEntity(form, charset))

execute(request)
execute(request).getString("access_token")
}

private def getRefreshedToken(refreshToken: String) = {
val request = new HttpPost(s"${config.get(KeycloakUrl)}/realms/${config.get(TokenRealm)}/protocol/openid-connect/token")
private def getRefreshedToken(refreshToken: String)(realm: String, client: String) = {
val refreshedCreds = refreshCredentials(refreshToken: String)(realm: String, client: String)

refreshedCreds.getString("access_token")
}

private def refreshCredentials(refreshToken: String)(realm: String, client: String) = {
val request = new HttpPost(s"${config.get(KeycloakUrl)}/realms/$realm/protocol/openid-connect/token")

val form = new java.util.ArrayList[BasicNameValuePair]()
form.add(new BasicNameValuePair("client_id", config.get(TokenClientId)))
form.add(new BasicNameValuePair("client_id", client))
form.add(new BasicNameValuePair("grant_type", "refresh_token"))
form.add(new BasicNameValuePair("refresh_token", refreshToken))

Expand All @@ -100,16 +112,16 @@ class KeycloakClient(config: UserConfig) extends BaseHttpClient with IKeycloak {

request.setEntity(new UrlEncodedFormEntity(form, charset))

execute(request)
execute(request).getString("access_token")
}

private def execute(request: HttpPost) = {
private def execute(request: HttpPost): JSONObject = {
val (body, status) = executeHttpRequest(request)
val token = status match {
case 200 => body.map(new JSONObject(_)).get.getString("access_token")
val parsedBody = status match {
case 200 => body.map(new JSONObject(_)).get
case _ => throw new IllegalStateException(formatExceptionMessage("Failed to get access token", status, body))
}
token
parsedBody
}

private def executeNoVerify(request: HttpPost) = {
Expand Down Expand Up @@ -142,7 +154,8 @@ class KeycloakClient(config: UserConfig) extends BaseHttpClient with IKeycloak {
import cats.effect.unsafe.implicits.global
retryingOnFailures(policyDelay.join(policyMaxRetries(expiresIn)), isResultOk, onFailure)(IO(executeNoVerify(request)))
.map { case(_, body) =>
body.map(new JSONObject(_)).get.getString("access_token")
val parsedBody = body.map(new JSONObject(_)).get
(parsedBody.getString("access_token"), parsedBody.getString("refresh_token"))
}.unsafeRunSync()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ca.ferlab.ferload.client.clients

import scala.util.control.NoStackTrace

class UnauthorizedException(message: String) extends RuntimeException with NoStackTrace {
override def getMessage: String = message
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ trait IKeycloak {

def getDevice: JSONObject

def getUserDeviceToken(deviceCode: String, expiresIn: Int): String
def getUserDeviceToken(deviceCode: String, expiresIn: Int): (String, String)

def getRefreshedTokens(refreshToken: String)(realm: String, client: String): (String, String)

def isValidToken(token: String): Boolean
}
34 changes: 26 additions & 8 deletions src/main/scala/ca/ferlab/ferload/client/commands/Download.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import picocli.CommandLine.{Command, IExitCodeGenerator, Option}

import java.io.{File, FileReader}
import java.util.Optional
import java.util.stream.Collectors.{summingInt, toList}
import java.util.stream.Collectors.toList
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success, Try, Using}

Expand Down Expand Up @@ -62,6 +62,7 @@ class Download(userConfig: UserConfig,

val authMethod = userConfig.get(Method)
var token = userConfig.get(Token)
val refreshToken = userConfig.get(RefreshToken)
if (KeycloakClient.AUTH_METHOD_PASSWORD == authMethod) {
if (!keycloak.isValidToken(token)) {
val passwordStr = password.orElseGet(() => readLine("-p", "", password = true))
Expand All @@ -82,17 +83,34 @@ class Download(userConfig: UserConfig,
keycloak.getUserCredentials(null, null, userConfig.get(Token))
}
} else if (KeycloakClient.AUTH_DEVICE == authMethod) {
val resp = CommandBlock("Retrieve device token", successEmoji, padding) {
keycloak.getDevice
}
if(keycloak.isValidToken(token)) {
CommandBlock("Re-use user device credentials", successEmoji, padding) {
""
}
} else {
val (newToken, newRefreshToken) = if(!keycloak.isValidToken(refreshToken)){
val resp = CommandBlock("Retrieve device token", successEmoji, padding) {
keycloak.getDevice
}

CommandBlock("Copy/Paste this URL in your browser and login please: ", successEmoji, padding) {
println(resp.getString("verification_uri_complete"))
}
keycloak.getUserDeviceToken(resp.getString("device_code"), resp.getInt("expires_in"))
} else {
CommandBlock("Refresh device credentials", successEmoji, padding) {
""
}
keycloak.getRefreshedTokens(refreshToken)(userConfig.get(KeycloakRealm), userConfig.get(KeycloakAudience))
}

token = CommandBlock("Copy/Paste this URL in your browser and login please: ", successEmoji, padding) {
println(resp.getString("verification_uri_complete"))
val newToken = keycloak.getUserDeviceToken(resp.getString("device_code"), resp.getInt("expires_in"))
userConfig.set(Token, newToken)
userConfig.set(RefreshToken, newRefreshToken)
userConfig.save()
newToken
token = newToken

}

}

val links: Map[LineContent, String] = CommandBlock("Retrieve Ferload download link(s)", successEmoji, padding) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ case object Username extends UserConfigName("username")

case object Token extends UserConfigName("token")

case object RefreshToken extends UserConfigName("refresh_token")

case object KeycloakUrl extends UserConfigName("keycloak-url")

case object KeycloakRealm extends UserConfigName("keycloak-realm")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ class DownloadTest extends AnyFunSuite with BeforeAndAfter {

override def getDevice: JSONObject = ???

override def getUserDeviceToken(deviceCode: String, expiresIn: Int): String = ???
override def getUserDeviceToken(deviceCode: String, expiresIn: Int): (String, String) = ???

override def getRefreshedTokens(refreshToken: String)(realm: String, client: String): (String, String) = ???
}

val mockKeycloakValidTokenInf: IKeycloak = new IKeycloak {
Expand All @@ -77,7 +79,9 @@ class DownloadTest extends AnyFunSuite with BeforeAndAfter {

override def getDevice: JSONObject = ???

override def getUserDeviceToken(deviceCode: String, expiresIn: Int): String = ???
override def getUserDeviceToken(deviceCode: String, expiresIn: Int): (String, String) = ???

override def getRefreshedTokens(refreshToken: String)(realm: String, client: String): (String, String) = ???
}

val mockFerload: IFerload = new IFerload {
Expand Down

0 comments on commit 8130718

Please sign in to comment.