mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 11:06:49 +00:00
Merge remote-tracking branch 'Hongwei/develop-Simon' into develop
This commit is contained in:
commit
f14c7b00a9
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.github/*
|
||||
*.class
|
||||
*.db
|
||||
.DS_Store
|
||||
|
||||
@ -76,7 +76,7 @@ MAVEN_OPTS="-Xms3G -Xmx6G -XX:MaxMetaspaceSize=2G" mvn -pl obp-http4s-runner -am
|
||||
java -jar obp-http4s-runner/target/obp-http4s-runner.jar
|
||||
```
|
||||
|
||||
The http4s server binds to `http4s.host` / `http4s.port` as configured in your props file (defaults are `127.0.0.1` and `8181`).
|
||||
The http4s server binds to `http4s.host` / `http4s.port` as configured in your props file (defaults are `127.0.0.1` and `8086`).
|
||||
|
||||
### ZED IDE Setup
|
||||
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to flush Redis, build the project, and run Jetty
|
||||
# Script to flush Redis, build the project, and run both Jetty and http4s servers
|
||||
#
|
||||
# This script should be run from the OBP-API root directory:
|
||||
# cd /path/to/OBP-API
|
||||
# ./flushall_build_and_run.sh
|
||||
#
|
||||
# The http4s server will run in the background on port 8081
|
||||
# The Jetty server will run in the foreground on port 8080
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
@ -27,4 +30,29 @@ echo "=========================================="
|
||||
echo "Building and running with Maven..."
|
||||
echo "=========================================="
|
||||
export MAVEN_OPTS="-Xss128m --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED"
|
||||
mvn install -pl .,obp-commons && mvn jetty:run -pl obp-api
|
||||
mvn install -pl .,obp-commons
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Building http4s runner..."
|
||||
echo "=========================================="
|
||||
export MAVEN_OPTS="-Xms3G -Xmx6G -XX:MaxMetaspaceSize=2G"
|
||||
mvn -pl obp-http4s-runner -am clean package -DskipTests=true -Dmaven.test.skip=true
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Starting http4s server in background..."
|
||||
echo "=========================================="
|
||||
java -jar obp-http4s-runner/target/obp-http4s-runner.jar > http4s-server.log 2>&1 &
|
||||
HTTP4S_PID=$!
|
||||
echo "http4s server started with PID: $HTTP4S_PID (port 8081)"
|
||||
echo "Logs are being written to: http4s-server.log"
|
||||
echo ""
|
||||
echo "To stop http4s server later: kill $HTTP4S_PID"
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "Starting Jetty server (foreground)..."
|
||||
echo "=========================================="
|
||||
export MAVEN_OPTS="-Xss128m --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED"
|
||||
mvn jetty:run -pl obp-api
|
||||
|
||||
@ -1691,6 +1691,6 @@ securelogging_mask_email=true
|
||||
############################################
|
||||
|
||||
# Host and port for http4s server (used by bootstrap.http4s.Http4sServer)
|
||||
# Defaults (if not set) are 127.0.0.1 and 8181
|
||||
# Defaults (if not set) are 127.0.0.1 and 8086
|
||||
http4s.host=127.0.0.1
|
||||
http4s.port=8086
|
||||
@ -17,8 +17,7 @@ object Http4sServer extends IOApp {
|
||||
val port = APIUtil.getPropsAsIntValue("http4s.port",8086)
|
||||
val host = APIUtil.getPropsValue("http4s.host","127.0.0.1")
|
||||
|
||||
val services: Kleisli[({type λ[β$0$] = OptionT[IO, β$0$]})#λ, Request[IO], Response[IO]] =
|
||||
code.api.v7_0_0.Http4s700.wrappedRoutesV700Services
|
||||
val services: HttpRoutes[IO] = code.api.v7_0_0.Http4s700.wrappedRoutesV700Services
|
||||
|
||||
val httpApp: Kleisli[IO, Request[IO], Response[IO]] = (services).orNotFound
|
||||
|
||||
|
||||
@ -228,6 +228,71 @@ object OAuth2Login extends RestHelper with MdcLoggable {
|
||||
|
||||
def urlOfJwkSets: Box[String] = Constant.oauth2JwkSetUrl
|
||||
|
||||
/**
|
||||
* Get all JWKS URLs from configuration.
|
||||
* This is a helper method for trying multiple JWKS URLs when validating tokens.
|
||||
* We need more than one JWKS URL if we have multiple OIDC providers configured etc.
|
||||
* @return List of all configured JWKS URLs
|
||||
*/
|
||||
protected def getAllJwksUrls: List[String] = {
|
||||
val url: List[String] = Constant.oauth2JwkSetUrl.toList
|
||||
url.flatMap(_.split(",").toList).map(_.trim).filter(_.nonEmpty)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to validate a JWT token with multiple JWKS URLs.
|
||||
* This is a generic retry mechanism that works for both ID tokens and access tokens.
|
||||
*
|
||||
* @param token The JWT token to validate
|
||||
* @param tokenType Description of token type for logging (e.g., "ID token", "access token")
|
||||
* @param validateFunc Function that validates token against a JWKS URL
|
||||
* @tparam T The type of claims returned (IDTokenClaimsSet or JWTClaimsSet)
|
||||
* @return Boxed claims or failure
|
||||
*/
|
||||
protected def tryValidateWithAllJwksUrls[T](
|
||||
token: String,
|
||||
tokenType: String,
|
||||
validateFunc: (String, String) => Box[T]
|
||||
): Box[T] = {
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - attempting to validate $tokenType")
|
||||
|
||||
// Extract issuer for better error reporting
|
||||
val actualIssuer = JwtUtil.getIssuer(token).getOrElse("NO_ISSUER_CLAIM")
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - JWT issuer claim: '$actualIssuer'")
|
||||
|
||||
// Get all JWKS URLs
|
||||
val allJwksUrls = getAllJwksUrls
|
||||
|
||||
if (allJwksUrls.isEmpty) {
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - No JWKS URLs configured")
|
||||
return Failure(Oauth2ThereIsNoUrlOfJwkSet)
|
||||
}
|
||||
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - Will try ${allJwksUrls.size} JWKS URL(s): $allJwksUrls")
|
||||
|
||||
// Try each JWKS URL until one succeeds
|
||||
val results = allJwksUrls.map { url =>
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - Trying JWKS URL: '$url'")
|
||||
val result = validateFunc(token, url)
|
||||
result match {
|
||||
case Full(_) =>
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - SUCCESS with JWKS URL: '$url'")
|
||||
case Failure(msg, _, _) =>
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - FAILED with JWKS URL: '$url', reason: $msg")
|
||||
case _ =>
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - FAILED with JWKS URL: '$url'")
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// Return the first successful result, or the last failure
|
||||
results.find(_.isDefined).getOrElse {
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - All ${allJwksUrls.size} JWKS URL(s) failed for issuer: '$actualIssuer'")
|
||||
logger.debug(s"tryValidateWithAllJwksUrls - Tried URLs: $allJwksUrls")
|
||||
results.lastOption.getOrElse(Failure(Oauth2ThereIsNoUrlOfJwkSet))
|
||||
}
|
||||
}
|
||||
|
||||
def checkUrlOfJwkSets(identityProvider: String) = {
|
||||
val url: List[String] = Constant.oauth2JwkSetUrl.toList
|
||||
val jwksUris: List[String] = url.map(_.toLowerCase()).map(_.split(",").toList).flatten
|
||||
@ -310,47 +375,10 @@ object OAuth2Login extends RestHelper with MdcLoggable {
|
||||
}.getOrElse(false)
|
||||
}
|
||||
def validateIdToken(idToken: String): Box[IDTokenClaimsSet] = {
|
||||
logger.debug(s"validateIdToken - attempting to validate ID token")
|
||||
|
||||
// Extract issuer for better error reporting
|
||||
val actualIssuer = JwtUtil.getIssuer(idToken).getOrElse("NO_ISSUER_CLAIM")
|
||||
logger.debug(s"validateIdToken - JWT issuer claim: '$actualIssuer'")
|
||||
|
||||
urlOfJwkSets match {
|
||||
case Full(url) =>
|
||||
logger.debug(s"validateIdToken - using JWKS URL: '$url'")
|
||||
JwtUtil.validateIdToken(idToken, url)
|
||||
case ParamFailure(a, b, c, apiFailure : APIFailure) =>
|
||||
logger.debug(s"validateIdToken - ParamFailure: $a, $b, $c, $apiFailure")
|
||||
logger.debug(s"validateIdToken - JWT issuer was: '$actualIssuer'")
|
||||
ParamFailure(a, b, c, apiFailure : APIFailure)
|
||||
case Failure(msg, t, c) =>
|
||||
logger.debug(s"validateIdToken - Failure getting JWKS URL: $msg")
|
||||
logger.debug(s"validateIdToken - JWT issuer was: '$actualIssuer'")
|
||||
if (msg.contains("OBP-20208")) {
|
||||
logger.debug("validateIdToken - OBP-20208 Error Details:")
|
||||
logger.debug(s"validateIdToken - JWT issuer claim: '$actualIssuer'")
|
||||
logger.debug(s"validateIdToken - oauth2.jwk_set.url value: '${Constant.oauth2JwkSetUrl}'")
|
||||
logger.debug("validateIdToken - Check that the JWKS URL configuration matches the JWT issuer")
|
||||
}
|
||||
Failure(msg, t, c)
|
||||
case _ =>
|
||||
logger.debug("validateIdToken - No JWKS URL available")
|
||||
logger.debug(s"validateIdToken - JWT issuer was: '$actualIssuer'")
|
||||
Failure(Oauth2ThereIsNoUrlOfJwkSet)
|
||||
}
|
||||
tryValidateWithAllJwksUrls(idToken, "ID token", JwtUtil.validateIdToken)
|
||||
}
|
||||
def validateAccessToken(accessToken: String): Box[JWTClaimsSet] = {
|
||||
urlOfJwkSets match {
|
||||
case Full(url) =>
|
||||
JwtUtil.validateAccessToken(accessToken, url)
|
||||
case ParamFailure(a, b, c, apiFailure : APIFailure) =>
|
||||
ParamFailure(a, b, c, apiFailure : APIFailure)
|
||||
case Failure(msg, t, c) =>
|
||||
Failure(msg, t, c)
|
||||
case _ =>
|
||||
Failure(Oauth2ThereIsNoUrlOfJwkSet)
|
||||
}
|
||||
tryValidateWithAllJwksUrls(accessToken, "access token", JwtUtil.validateAccessToken)
|
||||
}
|
||||
/** New Style Endpoints
|
||||
* This function creates user based on "iss" and "sub" fields
|
||||
|
||||
@ -208,11 +208,16 @@ object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with Md
|
||||
val resourceDocsJson = JSONFactory1_4_0.createResourceDocsJson(resourceDocs, isVersion4OrHigher, locale)
|
||||
resourceDocsJson.resource_docs
|
||||
case _ =>
|
||||
// Get all resource docs for the requested version
|
||||
val allResourceDocs = ImplementationsResourceDocs.getResourceDocsList(requestedApiVersion).getOrElse(List.empty)
|
||||
val filteredResourceDocs = ResourceDocsAPIMethodsUtil.filterResourceDocs(allResourceDocs, resourceDocTags, partialFunctions)
|
||||
val resourceDocJson = JSONFactory1_4_0.createResourceDocsJson(filteredResourceDocs, isVersion4OrHigher, locale)
|
||||
resourceDocJson.resource_docs
|
||||
contentParam match {
|
||||
case Some(DYNAMIC) =>
|
||||
ImplementationsResourceDocs.getResourceDocsObpDynamicCached(resourceDocTags, partialFunctions, locale, None, isVersion4OrHigher).head.resource_docs
|
||||
case Some(STATIC) => {
|
||||
ImplementationsResourceDocs.getStaticResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, isVersion4OrHigher).head.resource_docs
|
||||
}
|
||||
case _ => {
|
||||
ImplementationsResourceDocs.getAllResourceDocsObpCached(requestedApiVersionString, resourceDocTags, partialFunctions, locale, contentParam, isVersion4OrHigher).head.resource_docs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val hostname = HostName
|
||||
|
||||
@ -230,7 +230,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
|
||||
* @param contentParam if this is Some(`true`), only show dynamic endpoints, if Some(`false`), only show static. If it is None, we will show all. default is None
|
||||
* @return
|
||||
*/
|
||||
private def getStaticResourceDocsObpCached(
|
||||
def getStaticResourceDocsObpCached(
|
||||
requestedApiVersionString: String,
|
||||
resourceDocTags: Option[List[ResourceDocTag]],
|
||||
partialFunctionNames: Option[List[String]],
|
||||
@ -250,7 +250,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
|
||||
* @param contentParam if this is Some(`true`), only show dynamic endpoints, if Some(`false`), only show static. If it is None, we will show all. default is None
|
||||
* @return
|
||||
*/
|
||||
private def getAllResourceDocsObpCached(
|
||||
def getAllResourceDocsObpCached(
|
||||
requestedApiVersionString: String,
|
||||
resourceDocTags: Option[List[ResourceDocTag]],
|
||||
partialFunctionNames: Option[List[String]],
|
||||
@ -293,7 +293,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth
|
||||
|
||||
}
|
||||
|
||||
private def getResourceDocsObpDynamicCached(
|
||||
def getResourceDocsObpDynamicCached(
|
||||
resourceDocTags: Option[List[ResourceDocTag]],
|
||||
partialFunctionNames: Option[List[String]],
|
||||
locale: Option[String],
|
||||
|
||||
@ -25,4 +25,22 @@ object InMemory extends MdcLoggable {
|
||||
logger.trace(s"InMemory.memoizeWithInMemory.underlyingGuavaCache size ${underlyingGuavaCache.size()}, current cache key is $cacheKey")
|
||||
memoize(ttl)(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Count keys matching a pattern in the in-memory cache
|
||||
* @param pattern Pattern to match (supports * wildcard)
|
||||
* @return Number of matching keys
|
||||
*/
|
||||
def countKeys(pattern: String): Int = {
|
||||
try {
|
||||
val regex = pattern.replace("*", ".*").r
|
||||
val allKeys = underlyingGuavaCache.asMap().keySet()
|
||||
import scala.collection.JavaConverters._
|
||||
allKeys.asScala.count(key => regex.pattern.matcher(key).matches())
|
||||
} catch {
|
||||
case e: Throwable =>
|
||||
logger.error(s"Error counting in-memory cache keys for pattern $pattern: ${e.getMessage}")
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,12 +73,12 @@ object RedisLogger {
|
||||
|
||||
/** Map a LogLevel to its required entitlements */
|
||||
def requiredRoles(level: LogLevel): List[ApiRole] = level match {
|
||||
case TRACE => List(canGetTraceLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case DEBUG => List(canGetDebugLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case INFO => List(canGetInfoLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case WARNING => List(canGetWarningLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case ERROR => List(canGetErrorLevelLogsAtAllBanks, canGetAllLevelLogsAtAllBanks)
|
||||
case ALL => List(canGetAllLevelLogsAtAllBanks)
|
||||
case TRACE => List(canGetSystemLogCacheTrace, canGetSystemLogCacheAll)
|
||||
case DEBUG => List(canGetSystemLogCacheDebug, canGetSystemLogCacheAll)
|
||||
case INFO => List(canGetSystemLogCacheInfo, canGetSystemLogCacheAll)
|
||||
case WARNING => List(canGetSystemLogCacheWarning, canGetSystemLogCacheAll)
|
||||
case ERROR => List(canGetSystemLogCacheError, canGetSystemLogCacheAll)
|
||||
case ALL => List(canGetSystemLogCacheAll)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -107,35 +107,23 @@ object ApiRole extends MdcLoggable{
|
||||
|
||||
|
||||
// TRACE
|
||||
case class CanGetTraceLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetTraceLevelLogsAtOneBank = CanGetTraceLevelLogsAtOneBank()
|
||||
case class CanGetTraceLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetTraceLevelLogsAtAllBanks = CanGetTraceLevelLogsAtAllBanks()
|
||||
case class CanGetSystemLogCacheTrace(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetSystemLogCacheTrace = CanGetSystemLogCacheTrace()
|
||||
// DEBUG
|
||||
case class CanGetDebugLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetDebugLevelLogsAtOneBank = CanGetDebugLevelLogsAtOneBank()
|
||||
case class CanGetDebugLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetDebugLevelLogsAtAllBanks = CanGetDebugLevelLogsAtAllBanks()
|
||||
case class CanGetSystemLogCacheDebug(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetSystemLogCacheDebug = CanGetSystemLogCacheDebug()
|
||||
// INFO
|
||||
case class CanGetInfoLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetInfoLevelLogsAtOneBank = CanGetInfoLevelLogsAtOneBank()
|
||||
case class CanGetInfoLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetInfoLevelLogsAtAllBanks = CanGetInfoLevelLogsAtAllBanks()
|
||||
case class CanGetSystemLogCacheInfo(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetSystemLogCacheInfo = CanGetSystemLogCacheInfo()
|
||||
// WARNING
|
||||
case class CanGetWarningLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetWarningLevelLogsAtOneBank = CanGetWarningLevelLogsAtOneBank()
|
||||
case class CanGetWarningLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetWarningLevelLogsAtAllBanks = CanGetWarningLevelLogsAtAllBanks()
|
||||
case class CanGetSystemLogCacheWarning(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetSystemLogCacheWarning = CanGetSystemLogCacheWarning()
|
||||
// ERROR
|
||||
case class CanGetErrorLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetErrorLevelLogsAtOneBank = CanGetErrorLevelLogsAtOneBank()
|
||||
case class CanGetErrorLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetErrorLevelLogsAtAllBanks = CanGetErrorLevelLogsAtAllBanks()
|
||||
case class CanGetSystemLogCacheError(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetSystemLogCacheError = CanGetSystemLogCacheError()
|
||||
// ALL
|
||||
case class CanGetAllLevelLogsAtOneBank(requiresBankId: Boolean = true) extends ApiRole
|
||||
lazy val canGetAllLevelLogsAtOneBank = CanGetAllLevelLogsAtOneBank()
|
||||
case class CanGetAllLevelLogsAtAllBanks(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetAllLevelLogsAtAllBanks = CanGetAllLevelLogsAtAllBanks()
|
||||
case class CanGetSystemLogCacheAll(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canGetSystemLogCacheAll = CanGetSystemLogCacheAll()
|
||||
|
||||
case class CanUpdateAgentStatusAtAnyBank(requiresBankId: Boolean = false) extends ApiRole
|
||||
lazy val canUpdateAgentStatusAtAnyBank = CanUpdateAgentStatusAtAnyBank()
|
||||
|
||||
@ -91,6 +91,7 @@ object ApiTag {
|
||||
val apiTagDevOps = ResourceDocTag("DevOps")
|
||||
val apiTagSystem = ResourceDocTag("System")
|
||||
val apiTagCache = ResourceDocTag("Cache")
|
||||
val apiTagLogCache = ResourceDocTag("Log-Cache")
|
||||
|
||||
val apiTagApiCollection = ResourceDocTag("Api-Collection")
|
||||
|
||||
|
||||
@ -84,6 +84,7 @@ object ErrorMessages {
|
||||
val FXCurrencyCodeCombinationsNotSupported = "OBP-10004: ISO Currency code combination not supported for FX. Please modify the FROM_CURRENCY_CODE or TO_CURRENCY_CODE. "
|
||||
val InvalidDateFormat = "OBP-10005: Invalid Date Format. Could not convert value to a Date."
|
||||
val InvalidCurrency = "OBP-10006: Invalid Currency Value."
|
||||
val InvalidCacheNamespaceId = "OBP-10123: Invalid namespace_id."
|
||||
val IncorrectRoleName = "OBP-10007: Incorrect Role name:"
|
||||
val CouldNotTransformJsonToInternalModel = "OBP-10008: Could not transform Json to internal model."
|
||||
val CountNotSaveOrUpdateResource = "OBP-10009: Could not save or update resource."
|
||||
@ -269,7 +270,7 @@ object ErrorMessages {
|
||||
val Oauth2ThereIsNoUrlOfJwkSet = "OBP-20203: There is no an URL of OAuth 2.0 server's JWK set, published at a well-known URL."
|
||||
val Oauth2BadJWTException = "OBP-20204: Bad JWT error. "
|
||||
val Oauth2ParseException = "OBP-20205: Parse error. "
|
||||
val Oauth2BadJOSEException = "OBP-20206: Bad JSON Object Signing and Encryption (JOSE) exception. The ID token is invalid or expired. "
|
||||
val Oauth2BadJOSEException = "OBP-20206: Bad JSON Object Signing and Encryption (JOSE) exception. The ID token is invalid or expired. OBP-API Admin should check the oauth2.jwk_set.url list contains the jwks url of the provider."
|
||||
val Oauth2JOSEException = "OBP-20207: Bad JSON Object Signing and Encryption (JOSE) exception. An internal JOSE exception was encountered. "
|
||||
val Oauth2CannotMatchIssuerAndJwksUriException = "OBP-20208: Cannot match the issuer and JWKS URI at this server instance. "
|
||||
val Oauth2TokenHaveNoConsumer = "OBP-20209: The token have no linked consumer. "
|
||||
|
||||
@ -238,55 +238,204 @@ trait APIMethods510 {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to avoid code duplication
|
||||
private def getLogCacheHelper(level: RedisLogger.LogLevel.Value, cc: CallContext): Future[(RedisLogger.LogTail, Option[CallContext])] = {
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url)
|
||||
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext)
|
||||
limit = obpQueryParams.collectFirst { case OBPLimit(value) => value }
|
||||
offset = obpQueryParams.collectFirst { case OBPOffset(value) => value }
|
||||
logs <- Future(RedisLogger.getLogTail(level, limit, offset))
|
||||
} yield {
|
||||
(logs, HttpCode.`200`(callContext))
|
||||
}
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
logCacheEndpoint,
|
||||
logCacheTraceEndpoint,
|
||||
implementedInApiVersion,
|
||||
nameOf(logCacheEndpoint),
|
||||
nameOf(logCacheTraceEndpoint),
|
||||
"GET",
|
||||
"/system/log-cache/LOG_LEVEL",
|
||||
"Get Log Cache",
|
||||
"""Returns information about:
|
||||
|
|
||||
|* Log Cache
|
||||
"/system/log-cache/trace",
|
||||
"Get Trace Level Log Cache",
|
||||
"""Returns TRACE level logs from the system log cache.
|
||||
|
|
||||
|This endpoint supports pagination via the following optional query parameters:
|
||||
|* limit - Maximum number of log entries to return
|
||||
|* offset - Number of log entries to skip (for pagination)
|
||||
|
|
||||
|Example: GET /system/log-cache/INFO?limit=50&offset=100
|
||||
|Example: GET /system/log-cache/trace?limit=50&offset=100
|
||||
""",
|
||||
EmptyBody,
|
||||
EmptyBody,
|
||||
List($UserNotLoggedIn, UnknownError),
|
||||
apiTagSystem :: apiTagApi :: Nil,
|
||||
Some(List(canGetAllLevelLogsAtAllBanks)))
|
||||
apiTagSystem :: apiTagApi :: apiTagLogCache :: Nil,
|
||||
Some(List(canGetSystemLogCacheTrace, canGetSystemLogCacheAll)))
|
||||
|
||||
lazy val logCacheEndpoint: OBPEndpoint = {
|
||||
case "system" :: "log-cache" :: logLevel :: Nil JsonGet _ =>
|
||||
lazy val logCacheTraceEndpoint: OBPEndpoint = {
|
||||
case "system" :: "log-cache" :: "trace" :: Nil JsonGet _ =>
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
// Parse and validate log level
|
||||
level <- NewStyle.function.tryons(ErrorMessages.invalidLogLevel, 400, cc.callContext) {
|
||||
RedisLogger.LogLevel.valueOf(logLevel)
|
||||
}
|
||||
// Check entitlements using helper
|
||||
_ <- NewStyle.function.handleEntitlementsAndScopes(
|
||||
bankId = "",
|
||||
userId = cc.userId,
|
||||
roles = RedisLogger.LogLevel.requiredRoles(level),
|
||||
callContext = cc.callContext
|
||||
)
|
||||
httpParams <- NewStyle.function.extractHttpParamsFromUrl(cc.url)
|
||||
(obpQueryParams, callContext) <- createQueriesByHttpParamsFuture(httpParams, cc.callContext)
|
||||
// Extract limit and offset from query parameters
|
||||
limit = obpQueryParams.collectFirst { case OBPLimit(value) => value }
|
||||
offset = obpQueryParams.collectFirst { case OBPOffset(value) => value }
|
||||
// Fetch logs with pagination
|
||||
logs <- Future(RedisLogger.getLogTail(level, limit, offset))
|
||||
} yield {
|
||||
(logs, HttpCode.`200`(cc.callContext))
|
||||
}
|
||||
_ <- NewStyle.function.handleEntitlementsAndScopes("", cc.userId, List(canGetSystemLogCacheTrace, canGetSystemLogCacheAll), cc.callContext)
|
||||
result <- getLogCacheHelper(RedisLogger.LogLevel.TRACE, cc)
|
||||
} yield result
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
logCacheDebugEndpoint,
|
||||
implementedInApiVersion,
|
||||
nameOf(logCacheDebugEndpoint),
|
||||
"GET",
|
||||
"/system/log-cache/debug",
|
||||
"Get Debug Level Log Cache",
|
||||
"""Returns DEBUG level logs from the system log cache.
|
||||
|
|
||||
|This endpoint supports pagination via the following optional query parameters:
|
||||
|* limit - Maximum number of log entries to return
|
||||
|* offset - Number of log entries to skip (for pagination)
|
||||
|
|
||||
|Example: GET /system/log-cache/debug?limit=50&offset=100
|
||||
""",
|
||||
EmptyBody,
|
||||
EmptyBody,
|
||||
List($UserNotLoggedIn, UnknownError),
|
||||
apiTagSystem :: apiTagApi :: apiTagLogCache :: Nil,
|
||||
Some(List(canGetSystemLogCacheDebug, canGetSystemLogCacheAll)))
|
||||
|
||||
lazy val logCacheDebugEndpoint: OBPEndpoint = {
|
||||
case "system" :: "log-cache" :: "debug" :: Nil JsonGet _ =>
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
_ <- NewStyle.function.handleEntitlementsAndScopes("", cc.userId, List(canGetSystemLogCacheDebug, canGetSystemLogCacheAll), cc.callContext)
|
||||
result <- getLogCacheHelper(RedisLogger.LogLevel.DEBUG, cc)
|
||||
} yield result
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
logCacheInfoEndpoint,
|
||||
implementedInApiVersion,
|
||||
nameOf(logCacheInfoEndpoint),
|
||||
"GET",
|
||||
"/system/log-cache/info",
|
||||
"Get Info Level Log Cache",
|
||||
"""Returns INFO level logs from the system log cache.
|
||||
|
|
||||
|This endpoint supports pagination via the following optional query parameters:
|
||||
|* limit - Maximum number of log entries to return
|
||||
|* offset - Number of log entries to skip (for pagination)
|
||||
|
|
||||
|Example: GET /system/log-cache/info?limit=50&offset=100
|
||||
""",
|
||||
EmptyBody,
|
||||
EmptyBody,
|
||||
List($UserNotLoggedIn, UnknownError),
|
||||
apiTagSystem :: apiTagApi :: apiTagLogCache :: Nil,
|
||||
Some(List(canGetSystemLogCacheInfo, canGetSystemLogCacheAll)))
|
||||
|
||||
lazy val logCacheInfoEndpoint: OBPEndpoint = {
|
||||
case "system" :: "log-cache" :: "info" :: Nil JsonGet _ =>
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
_ <- NewStyle.function.handleEntitlementsAndScopes("", cc.userId, List(canGetSystemLogCacheInfo, canGetSystemLogCacheAll), cc.callContext)
|
||||
result <- getLogCacheHelper(RedisLogger.LogLevel.INFO, cc)
|
||||
} yield result
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
logCacheWarningEndpoint,
|
||||
implementedInApiVersion,
|
||||
nameOf(logCacheWarningEndpoint),
|
||||
"GET",
|
||||
"/system/log-cache/warning",
|
||||
"Get Warning Level Log Cache",
|
||||
"""Returns WARNING level logs from the system log cache.
|
||||
|
|
||||
|This endpoint supports pagination via the following optional query parameters:
|
||||
|* limit - Maximum number of log entries to return
|
||||
|* offset - Number of log entries to skip (for pagination)
|
||||
|
|
||||
|Example: GET /system/log-cache/warning?limit=50&offset=100
|
||||
""",
|
||||
EmptyBody,
|
||||
EmptyBody,
|
||||
List($UserNotLoggedIn, UnknownError),
|
||||
apiTagSystem :: apiTagApi :: apiTagLogCache :: Nil,
|
||||
Some(List(canGetSystemLogCacheWarning, canGetSystemLogCacheAll)))
|
||||
|
||||
lazy val logCacheWarningEndpoint: OBPEndpoint = {
|
||||
case "system" :: "log-cache" :: "warning" :: Nil JsonGet _ =>
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
_ <- NewStyle.function.handleEntitlementsAndScopes("", cc.userId, List(canGetSystemLogCacheWarning, canGetSystemLogCacheAll), cc.callContext)
|
||||
result <- getLogCacheHelper(RedisLogger.LogLevel.WARNING, cc)
|
||||
} yield result
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
logCacheErrorEndpoint,
|
||||
implementedInApiVersion,
|
||||
nameOf(logCacheErrorEndpoint),
|
||||
"GET",
|
||||
"/system/log-cache/error",
|
||||
"Get Error Level Log Cache",
|
||||
"""Returns ERROR level logs from the system log cache.
|
||||
|
|
||||
|This endpoint supports pagination via the following optional query parameters:
|
||||
|* limit - Maximum number of log entries to return
|
||||
|* offset - Number of log entries to skip (for pagination)
|
||||
|
|
||||
|Example: GET /system/log-cache/error?limit=50&offset=100
|
||||
""",
|
||||
EmptyBody,
|
||||
EmptyBody,
|
||||
List($UserNotLoggedIn, UnknownError),
|
||||
apiTagSystem :: apiTagApi :: apiTagLogCache :: Nil,
|
||||
Some(List(canGetSystemLogCacheError, canGetSystemLogCacheAll)))
|
||||
|
||||
lazy val logCacheErrorEndpoint: OBPEndpoint = {
|
||||
case "system" :: "log-cache" :: "error" :: Nil JsonGet _ =>
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
_ <- NewStyle.function.handleEntitlementsAndScopes("", cc.userId, List(canGetSystemLogCacheError, canGetSystemLogCacheAll), cc.callContext)
|
||||
result <- getLogCacheHelper(RedisLogger.LogLevel.ERROR, cc)
|
||||
} yield result
|
||||
}
|
||||
|
||||
staticResourceDocs += ResourceDoc(
|
||||
logCacheAllEndpoint,
|
||||
implementedInApiVersion,
|
||||
nameOf(logCacheAllEndpoint),
|
||||
"GET",
|
||||
"/system/log-cache/all",
|
||||
"Get All Level Log Cache",
|
||||
"""Returns logs of all levels from the system log cache.
|
||||
|
|
||||
|This endpoint supports pagination via the following optional query parameters:
|
||||
|* limit - Maximum number of log entries to return
|
||||
|* offset - Number of log entries to skip (for pagination)
|
||||
|
|
||||
|Example: GET /system/log-cache/all?limit=50&offset=100
|
||||
""",
|
||||
EmptyBody,
|
||||
EmptyBody,
|
||||
List($UserNotLoggedIn, UnknownError),
|
||||
apiTagSystem :: apiTagApi :: apiTagLogCache :: Nil,
|
||||
Some(List(canGetSystemLogCacheAll)))
|
||||
|
||||
lazy val logCacheAllEndpoint: OBPEndpoint = {
|
||||
case "system" :: "log-cache" :: "all" :: Nil JsonGet _ =>
|
||||
cc =>
|
||||
implicit val ec = EndpointContext(Some(cc))
|
||||
for {
|
||||
_ <- NewStyle.function.handleEntitlementsAndScopes("", cc.userId, List(canGetSystemLogCacheAll), cc.callContext)
|
||||
result <- getLogCacheHelper(RedisLogger.LogLevel.ALL, cc)
|
||||
} yield result
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
|
||||
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
|
||||
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
|
||||
import code.api.v6_0_0.JSONFactory600.{AddUserToGroupResponseJsonV600, DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupEntitlementJsonV600, GroupEntitlementsJsonV600, GroupJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, UserGroupMembershipJsonV600, UserGroupMembershipsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createAbacRuleJsonV600, createAbacRulesJsonV600, createActiveRateLimitsJsonV600, createCallLimitJsonV600, createRedisCallCountersJson}
|
||||
import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CacheConfigJsonV600, CacheInfoJsonV600, CacheNamespaceInfoJsonV600, CacheProviderConfigJsonV600, CreateAbacRuleJsonV600, CurrentConsumerJsonV600, ExecuteAbacRuleJsonV600, UpdateAbacRuleJsonV600}
|
||||
import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CacheConfigJsonV600, CacheInfoJsonV600, CacheNamespaceInfoJsonV600, CreateAbacRuleJsonV600, CurrentConsumerJsonV600, ExecuteAbacRuleJsonV600, InMemoryCacheStatusJsonV600, RedisCacheStatusJsonV600, UpdateAbacRuleJsonV600}
|
||||
import code.api.v6_0_0.OBPAPI6_0_0
|
||||
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
|
||||
import code.metrics.APIMetrics
|
||||
@ -635,7 +635,7 @@ trait APIMethods600 {
|
||||
}
|
||||
namespaceId = postJson.namespace_id
|
||||
_ <- Helper.booleanToFuture(
|
||||
s"Invalid namespace_id: $namespaceId. Valid values: ${Constant.ALL_CACHE_NAMESPACES.mkString(", ")}",
|
||||
s"$InvalidCacheNamespaceId $namespaceId. Valid values: ${Constant.ALL_CACHE_NAMESPACES.mkString(", ")}",
|
||||
400,
|
||||
callContext
|
||||
)(Constant.ALL_CACHE_NAMESPACES.contains(namespaceId))
|
||||
@ -667,8 +667,8 @@ trait APIMethods600 {
|
||||
"Get Cache Configuration",
|
||||
"""Returns cache configuration information including:
|
||||
|
|
||||
|- Available cache providers (Redis, In-Memory)
|
||||
|- Redis connection details (URL, port, SSL)
|
||||
|- Redis status: availability, connection details (URL, port, SSL)
|
||||
|- In-memory cache status: availability and current size
|
||||
|- Instance ID and environment
|
||||
|- Global cache namespace prefix
|
||||
|
|
||||
@ -678,21 +678,15 @@ trait APIMethods600 {
|
||||
|""",
|
||||
EmptyBody,
|
||||
CacheConfigJsonV600(
|
||||
providers = List(
|
||||
CacheProviderConfigJsonV600(
|
||||
provider = "redis",
|
||||
enabled = true,
|
||||
url = Some("127.0.0.1"),
|
||||
port = Some(6379),
|
||||
use_ssl = Some(false)
|
||||
),
|
||||
CacheProviderConfigJsonV600(
|
||||
provider = "in_memory",
|
||||
enabled = true,
|
||||
url = None,
|
||||
port = None,
|
||||
use_ssl = None
|
||||
)
|
||||
redis_status = RedisCacheStatusJsonV600(
|
||||
available = true,
|
||||
url = "127.0.0.1",
|
||||
port = 6379,
|
||||
use_ssl = false
|
||||
),
|
||||
in_memory_status = InMemoryCacheStatusJsonV600(
|
||||
available = true,
|
||||
current_size = 42
|
||||
),
|
||||
instance_id = "obp",
|
||||
environment = "dev",
|
||||
@ -733,6 +727,14 @@ trait APIMethods600 {
|
||||
|- Current version counter
|
||||
|- Number of keys in each namespace
|
||||
|- Description and category
|
||||
|- Storage location (redis, memory, both, or unknown)
|
||||
| - "redis": Keys stored in Redis
|
||||
| - "memory": Keys stored in in-memory cache
|
||||
| - "both": Keys in both locations (indicates a BUG - should never happen)
|
||||
| - "unknown": No keys found, storage location cannot be determined
|
||||
|- TTL info: Sampled TTL information from actual keys
|
||||
| - Shows actual TTL values from up to 5 sample keys
|
||||
| - Format: "123s" (fixed), "range 60s to 3600s (avg 1800s)" (variable), "no expiry" (persistent)
|
||||
|- Total key count across all namespaces
|
||||
|- Redis availability status
|
||||
|
|
||||
@ -749,7 +751,9 @@ trait APIMethods600 {
|
||||
current_version = 1,
|
||||
key_count = 42,
|
||||
description = "Rate limit call counters",
|
||||
category = "Rate Limiting"
|
||||
category = "Rate Limiting",
|
||||
storage_location = "redis",
|
||||
ttl_info = "range 60s to 86400s (avg 3600s)"
|
||||
),
|
||||
CacheNamespaceInfoJsonV600(
|
||||
namespace_id = "rd_localised",
|
||||
@ -757,7 +761,9 @@ trait APIMethods600 {
|
||||
current_version = 1,
|
||||
key_count = 128,
|
||||
description = "Localized resource docs",
|
||||
category = "API Documentation"
|
||||
category = "API Documentation",
|
||||
storage_location = "redis",
|
||||
ttl_info = "3600s"
|
||||
)
|
||||
),
|
||||
total_keys = 170,
|
||||
|
||||
@ -268,16 +268,21 @@ case class InvalidatedCacheNamespaceJsonV600(
|
||||
status: String
|
||||
)
|
||||
|
||||
case class CacheProviderConfigJsonV600(
|
||||
provider: String,
|
||||
enabled: Boolean,
|
||||
url: Option[String],
|
||||
port: Option[Int],
|
||||
use_ssl: Option[Boolean]
|
||||
case class RedisCacheStatusJsonV600(
|
||||
available: Boolean,
|
||||
url: String,
|
||||
port: Int,
|
||||
use_ssl: Boolean
|
||||
)
|
||||
|
||||
case class InMemoryCacheStatusJsonV600(
|
||||
available: Boolean,
|
||||
current_size: Long
|
||||
)
|
||||
|
||||
case class CacheConfigJsonV600(
|
||||
providers: List[CacheProviderConfigJsonV600],
|
||||
redis_status: RedisCacheStatusJsonV600,
|
||||
in_memory_status: InMemoryCacheStatusJsonV600,
|
||||
instance_id: String,
|
||||
environment: String,
|
||||
global_prefix: String
|
||||
@ -289,7 +294,9 @@ case class CacheNamespaceInfoJsonV600(
|
||||
current_version: Long,
|
||||
key_count: Int,
|
||||
description: String,
|
||||
category: String
|
||||
category: String,
|
||||
storage_location: String,
|
||||
ttl_info: String
|
||||
)
|
||||
|
||||
case class CacheInfoJsonV600(
|
||||
@ -1119,21 +1126,17 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
|
||||
import code.api.Constant
|
||||
import net.liftweb.util.Props
|
||||
|
||||
val redisProvider = CacheProviderConfigJsonV600(
|
||||
provider = "redis",
|
||||
enabled = true,
|
||||
url = Some(Redis.url),
|
||||
port = Some(Redis.port),
|
||||
use_ssl = Some(Redis.useSsl)
|
||||
)
|
||||
val redisIsReady = try {
|
||||
Redis.isRedisReady
|
||||
} catch {
|
||||
case _: Throwable => false
|
||||
}
|
||||
|
||||
val inMemoryProvider = CacheProviderConfigJsonV600(
|
||||
provider = "in_memory",
|
||||
enabled = true,
|
||||
url = None,
|
||||
port = None,
|
||||
use_ssl = None
|
||||
)
|
||||
val inMemorySize = try {
|
||||
InMemory.underlyingGuavaCache.size()
|
||||
} catch {
|
||||
case _: Throwable => 0L
|
||||
}
|
||||
|
||||
val instanceId = code.api.util.APIUtil.getPropsValue("api_instance_id").getOrElse("obp")
|
||||
val environment = Props.mode match {
|
||||
@ -1144,8 +1147,21 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
|
||||
case _ => "unknown"
|
||||
}
|
||||
|
||||
val redisStatus = RedisCacheStatusJsonV600(
|
||||
available = redisIsReady,
|
||||
url = Redis.url,
|
||||
port = Redis.port,
|
||||
use_ssl = Redis.useSsl
|
||||
)
|
||||
|
||||
val inMemoryStatus = InMemoryCacheStatusJsonV600(
|
||||
available = inMemorySize >= 0,
|
||||
current_size = inMemorySize
|
||||
)
|
||||
|
||||
CacheConfigJsonV600(
|
||||
providers = List(redisProvider, inMemoryProvider),
|
||||
redis_status = redisStatus,
|
||||
in_memory_status = inMemoryStatus,
|
||||
instance_id = instanceId,
|
||||
environment = environment,
|
||||
global_prefix = Constant.getGlobalCacheNamespacePrefix
|
||||
@ -1153,8 +1169,9 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
|
||||
}
|
||||
|
||||
def createCacheInfoJsonV600(): CacheInfoJsonV600 = {
|
||||
import code.api.cache.Redis
|
||||
import code.api.cache.{Redis, InMemory}
|
||||
import code.api.Constant
|
||||
import code.api.JedisMethod
|
||||
|
||||
val namespaceDescriptions = Map(
|
||||
Constant.CALL_COUNTER_NAMESPACE -> ("Rate limit call counters", "Rate Limiting"),
|
||||
@ -1178,14 +1195,69 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
|
||||
val prefix = Constant.getVersionedCachePrefix(namespaceId)
|
||||
val pattern = s"${prefix}*"
|
||||
|
||||
val keyCount = try {
|
||||
val count = Redis.countKeys(pattern)
|
||||
totalKeys += count
|
||||
count
|
||||
// Dynamically determine storage location by checking where keys exist
|
||||
var redisKeyCount = 0
|
||||
var memoryKeyCount = 0
|
||||
var storageLocation = "unknown"
|
||||
var ttlInfo = "no keys to sample"
|
||||
|
||||
try {
|
||||
redisKeyCount = Redis.countKeys(pattern)
|
||||
totalKeys += redisKeyCount
|
||||
|
||||
// Sample keys to get TTL information
|
||||
if (redisKeyCount > 0) {
|
||||
val sampleKeys = Redis.scanKeys(pattern).take(5)
|
||||
val ttls = sampleKeys.flatMap { key =>
|
||||
Redis.use(JedisMethod.TTL, key, None, None).map(_.toLong)
|
||||
}
|
||||
|
||||
if (ttls.nonEmpty) {
|
||||
val minTtl = ttls.min
|
||||
val maxTtl = ttls.max
|
||||
val avgTtl = ttls.sum / ttls.length.toLong
|
||||
|
||||
ttlInfo = if (minTtl == maxTtl) {
|
||||
if (minTtl == -1) "no expiry"
|
||||
else if (minTtl == -2) "keys expired or missing"
|
||||
else s"${minTtl}s"
|
||||
} else {
|
||||
s"range ${minTtl}s to ${maxTtl}s (avg ${avgTtl}s)"
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case _: Throwable =>
|
||||
redisAvailable = false
|
||||
0
|
||||
}
|
||||
|
||||
try {
|
||||
memoryKeyCount = InMemory.countKeys(pattern)
|
||||
totalKeys += memoryKeyCount
|
||||
|
||||
if (memoryKeyCount > 0 && redisKeyCount == 0) {
|
||||
ttlInfo = "in-memory (no TTL in Guava cache)"
|
||||
}
|
||||
} catch {
|
||||
case _: Throwable =>
|
||||
// In-memory cache error (shouldn't happen, but handle gracefully)
|
||||
}
|
||||
|
||||
// Determine storage based on where keys actually exist
|
||||
val keyCount = if (redisKeyCount > 0 && memoryKeyCount > 0) {
|
||||
storageLocation = "both"
|
||||
ttlInfo = s"redis: ${ttlInfo}, memory: in-memory cache"
|
||||
redisKeyCount + memoryKeyCount
|
||||
} else if (redisKeyCount > 0) {
|
||||
storageLocation = "redis"
|
||||
redisKeyCount
|
||||
} else if (memoryKeyCount > 0) {
|
||||
storageLocation = "memory"
|
||||
memoryKeyCount
|
||||
} else {
|
||||
// No keys found in either location - we don't know where they would be stored
|
||||
storageLocation = "unknown"
|
||||
0
|
||||
}
|
||||
|
||||
val (description, category) = namespaceDescriptions.getOrElse(namespaceId, ("Unknown namespace", "Other"))
|
||||
@ -1196,7 +1268,9 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
|
||||
current_version = version,
|
||||
key_count = keyCount,
|
||||
description = description,
|
||||
category = category
|
||||
category = category,
|
||||
storage_location = storageLocation,
|
||||
ttl_info = ttlInfo
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package code.api.v5_1_0
|
||||
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.ApiRole.CanGetAllLevelLogsAtAllBanks
|
||||
import code.api.util.ApiRole.{CanGetSystemLogCacheAll,CanGetSystemLogCacheInfo}
|
||||
import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn}
|
||||
import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0
|
||||
import code.entitlement.Entitlement
|
||||
@ -21,12 +21,12 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
* This is made possible by the scalatest maven plugin
|
||||
*/
|
||||
object VersionOfApi extends Tag(ApiVersion.v5_1_0.toString)
|
||||
object ApiEndpoint1 extends Tag(nameOf(Implementations5_1_0.logCacheEndpoint))
|
||||
object ApiEndpoint1 extends Tag(nameOf(Implementations5_1_0.logCacheInfoEndpoint))
|
||||
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") {
|
||||
scenario("We will call the endpoint without user credentials", ApiEndpoint1, VersionOfApi) {
|
||||
When("We make a request v5.1.0")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "info").GET
|
||||
val response = makeGetRequest(request)
|
||||
Then("We should get a 401")
|
||||
response.code should equal(401)
|
||||
@ -37,21 +37,23 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Missing entitlement") {
|
||||
scenario("We will call the endpoint with user credentials but without proper entitlement", ApiEndpoint1, VersionOfApi) {
|
||||
When("We make a request v5.1.0")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1)
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1)
|
||||
val response = makeGetRequest(request)
|
||||
Then("error should be " + UserHasMissingRoles + CanGetAllLevelLogsAtAllBanks)
|
||||
Then("error should be " + UserHasMissingRoles + CanGetSystemLogCacheAll)
|
||||
response.code should equal(403)
|
||||
response.body.extract[ErrorMessage].message should be(UserHasMissingRoles + CanGetAllLevelLogsAtAllBanks)
|
||||
response.body.extract[ErrorMessage].message contains (UserHasMissingRoles) shouldBe (true)
|
||||
response.body.extract[ErrorMessage].message contains CanGetSystemLogCacheInfo.toString() shouldBe (true)
|
||||
response.body.extract[ErrorMessage].message contains CanGetSystemLogCacheAll.toString() shouldBe (true)
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access without pagination") {
|
||||
scenario("We get log cache without pagination parameters", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We make a request to get log cache")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1)
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1)
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a successful response")
|
||||
@ -66,10 +68,10 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access with limit parameter") {
|
||||
scenario("We get log cache with limit parameter only", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We make a request with limit parameter")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "5"))
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("limit", "5"))
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a successful response")
|
||||
@ -85,10 +87,10 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access with offset parameter") {
|
||||
scenario("We get log cache with offset parameter only", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We make a request with offset parameter")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("offset", "2"))
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("offset", "2"))
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a successful response")
|
||||
@ -103,10 +105,10 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access with both parameters") {
|
||||
scenario("We get log cache with both limit and offset parameters", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We make a request with both limit and offset parameters")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "3"), ("offset", "1"))
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("limit", "3"), ("offset", "1"))
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a successful response")
|
||||
@ -122,13 +124,13 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Edge cases") {
|
||||
scenario("We get error with zero limit (invalid parameter)", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We make a request with zero limit")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "0"))
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("limit", "0"))
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a bad request response")
|
||||
Then("We should get a not found response since endpoint does not exist")
|
||||
response.code should equal(400)
|
||||
val json = response.body.extract[JObject]
|
||||
|
||||
@ -139,10 +141,10 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
|
||||
scenario("We get log cache with large offset", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We make a request with very large offset")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("offset", "10000"))
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("offset", "10000"))
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a successful response")
|
||||
@ -156,10 +158,10 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
|
||||
scenario("We get log cache with minimum valid limit", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We make a request with minimum valid limit (1)")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "1"))
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("limit", "1"))
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a successful response")
|
||||
@ -175,10 +177,10 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Different log levels") {
|
||||
scenario("We test different log levels with pagination", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We make requests to different log levels with pagination")
|
||||
val logLevels = List("DEBUG", "INFO", "WARN", "ERROR", "ALL")
|
||||
val logLevels = List("debug", "info", "warning", "error", "all")
|
||||
|
||||
logLevels.foreach { logLevel =>
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / logLevel).GET <@(user1) <<? List(("limit", "2"), ("offset", "0"))
|
||||
@ -197,48 +199,48 @@ class LogCacheEndpointTest extends V510ServerSetup {
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Invalid log level") {
|
||||
scenario("We get error for invalid log level", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We make a request with invalid log level")
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "INVALID_LEVEL").GET <@(user1)
|
||||
val request = (v5_1_0_Request / "system" / "log-cache" / "invalid_level").GET <@(user1)
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a bad request response")
|
||||
response.code should equal(400)
|
||||
Then("We should get a not found response since endpoint does not exist")
|
||||
response.code should equal(404)
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Invalid parameters") {
|
||||
scenario("We test invalid pagination parameters", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with proper entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetAllLevelLogsAtAllBanks.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetSystemLogCacheAll.toString)
|
||||
|
||||
When("We test with non-numeric limit parameter")
|
||||
val requestInvalidLimit = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "abc"))
|
||||
val requestInvalidLimit = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("limit", "abc"))
|
||||
val responseInvalidLimit = makeGetRequest(requestInvalidLimit)
|
||||
|
||||
Then("We should get a bad request response")
|
||||
Then("We should get a not found response since endpoint does not exist")
|
||||
responseInvalidLimit.code should equal(400)
|
||||
|
||||
When("We test with non-numeric offset parameter")
|
||||
val requestInvalidOffset = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("offset", "xyz"))
|
||||
val requestInvalidOffset = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("offset", "xyz"))
|
||||
val responseInvalidOffset = makeGetRequest(requestInvalidOffset)
|
||||
|
||||
Then("We should get a bad request response")
|
||||
Then("We should get a not found response since endpoint does not exist")
|
||||
responseInvalidOffset.code should equal(400)
|
||||
|
||||
When("We test with negative limit parameter")
|
||||
val requestNegativeLimit = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("limit", "-1"))
|
||||
val requestNegativeLimit = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("limit", "-1"))
|
||||
val responseNegativeLimit = makeGetRequest(requestNegativeLimit)
|
||||
|
||||
Then("We should get a bad request response")
|
||||
Then("We should get a not found response since endpoint does not exist")
|
||||
responseNegativeLimit.code should equal(400)
|
||||
|
||||
When("We test with negative offset parameter")
|
||||
val requestNegativeOffset = (v5_1_0_Request / "system" / "log-cache" / "INFO").GET <@(user1) <<? List(("offset", "-1"))
|
||||
val requestNegativeOffset = (v5_1_0_Request / "system" / "log-cache" / "info").GET <@(user1) <<? List(("offset", "-1"))
|
||||
val responseNegativeOffset = makeGetRequest(requestNegativeOffset)
|
||||
|
||||
Then("We should get a bad request response")
|
||||
Then("We should get a not found response since endpoint does not exist")
|
||||
responseNegativeOffset.code should equal(400)
|
||||
}
|
||||
}
|
||||
|
||||
360
obp-api/src/test/scala/code/api/v6_0_0/CacheEndpointsTest.scala
Normal file
360
obp-api/src/test/scala/code/api/v6_0_0/CacheEndpointsTest.scala
Normal file
@ -0,0 +1,360 @@
|
||||
/**
|
||||
Open Bank Project - API
|
||||
Copyright (C) 2011-2024, TESOBE GmbH
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Email: contact@tesobe.com
|
||||
TESOBE GmbH
|
||||
Osloerstrasse 16/17
|
||||
Berlin 13359, Germany
|
||||
|
||||
This product includes software developed at
|
||||
TESOBE (http://www.tesobe.com/)
|
||||
*/
|
||||
package code.api.v6_0_0
|
||||
|
||||
import code.api.util.APIUtil.OAuth._
|
||||
import code.api.util.ApiRole.{CanGetCacheConfig, CanGetCacheInfo, CanInvalidateCacheNamespace}
|
||||
import code.api.util.ErrorMessages.{InvalidJsonFormat, UserHasMissingRoles, UserNotLoggedIn}
|
||||
import code.api.v6_0_0.OBPAPI6_0_0.Implementations6_0_0
|
||||
import code.entitlement.Entitlement
|
||||
import com.github.dwickern.macros.NameOf.nameOf
|
||||
import com.openbankproject.commons.model.ErrorMessage
|
||||
import com.openbankproject.commons.util.ApiVersion
|
||||
import net.liftweb.json.Serialization.write
|
||||
import org.scalatest.Tag
|
||||
|
||||
class CacheEndpointsTest extends V600ServerSetup {
|
||||
/**
|
||||
* Test tags
|
||||
* Example: To run tests with tag "getCacheConfig":
|
||||
* mvn test -D tagsToInclude
|
||||
*
|
||||
* This is made possible by the scalatest maven plugin
|
||||
*/
|
||||
object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString)
|
||||
object ApiEndpoint1 extends Tag(nameOf(Implementations6_0_0.getCacheConfig))
|
||||
object ApiEndpoint2 extends Tag(nameOf(Implementations6_0_0.getCacheInfo))
|
||||
object ApiEndpoint3 extends Tag(nameOf(Implementations6_0_0.invalidateCacheNamespace))
|
||||
|
||||
// ============================================================================================================
|
||||
// GET /system/cache/config - Get Cache Configuration
|
||||
// ============================================================================================================
|
||||
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Unauthorized access") {
|
||||
scenario("We call getCacheConfig without user credentials", ApiEndpoint1, VersionOfApi) {
|
||||
When("We make a request v6.0.0 without credentials")
|
||||
val request = (v6_0_0_Request / "system" / "cache" / "config").GET
|
||||
val response = makeGetRequest(request)
|
||||
Then("We should get a 401")
|
||||
response.code should equal(401)
|
||||
response.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Missing role") {
|
||||
scenario("We call getCacheConfig without the CanGetCacheConfig role", ApiEndpoint1, VersionOfApi) {
|
||||
When("We make a request v6.0.0 without the required role")
|
||||
val request = (v6_0_0_Request / "system" / "cache" / "config").GET <@ (user1)
|
||||
val response = makeGetRequest(request)
|
||||
Then("We should get a 403")
|
||||
response.code should equal(403)
|
||||
And("error should be " + UserHasMissingRoles + CanGetCacheConfig)
|
||||
response.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanGetCacheConfig)
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint1 version $VersionOfApi - Authorized access") {
|
||||
scenario("We call getCacheConfig with the CanGetCacheConfig role", ApiEndpoint1, VersionOfApi) {
|
||||
Given("We have a user with CanGetCacheConfig entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetCacheConfig.toString)
|
||||
|
||||
When("We make a request v6.0.0 with proper role")
|
||||
val request = (v6_0_0_Request / "system" / "cache" / "config").GET <@ (user1)
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a 200")
|
||||
response.code should equal(200)
|
||||
|
||||
And("The response should have the correct structure")
|
||||
val cacheConfig = response.body.extract[CacheConfigJsonV600]
|
||||
cacheConfig.instance_id should not be empty
|
||||
cacheConfig.environment should not be empty
|
||||
cacheConfig.global_prefix should not be empty
|
||||
|
||||
And("Redis status should have valid data")
|
||||
cacheConfig.redis_status.available shouldBe a[Boolean]
|
||||
cacheConfig.redis_status.url should not be empty
|
||||
cacheConfig.redis_status.port should be > 0
|
||||
cacheConfig.redis_status.use_ssl shouldBe a[Boolean]
|
||||
|
||||
And("In-memory status should have valid data")
|
||||
cacheConfig.in_memory_status.available shouldBe a[Boolean]
|
||||
cacheConfig.in_memory_status.current_size should be >= 0L
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================================================
|
||||
// GET /system/cache/info - Get Cache Information
|
||||
// ============================================================================================================
|
||||
|
||||
feature(s"test $ApiEndpoint2 version $VersionOfApi - Unauthorized access") {
|
||||
scenario("We call getCacheInfo without user credentials", ApiEndpoint2, VersionOfApi) {
|
||||
When("We make a request v6.0.0 without credentials")
|
||||
val request = (v6_0_0_Request / "system" / "cache" / "info").GET
|
||||
val response = makeGetRequest(request)
|
||||
Then("We should get a 401")
|
||||
response.code should equal(401)
|
||||
response.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint2 version $VersionOfApi - Missing role") {
|
||||
scenario("We call getCacheInfo without the CanGetCacheInfo role", ApiEndpoint2, VersionOfApi) {
|
||||
When("We make a request v6.0.0 without the required role")
|
||||
val request = (v6_0_0_Request / "system" / "cache" / "info").GET <@ (user1)
|
||||
val response = makeGetRequest(request)
|
||||
Then("We should get a 403")
|
||||
response.code should equal(403)
|
||||
And("error should be " + UserHasMissingRoles + CanGetCacheInfo)
|
||||
response.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanGetCacheInfo)
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint2 version $VersionOfApi - Authorized access") {
|
||||
scenario("We call getCacheInfo with the CanGetCacheInfo role", ApiEndpoint2, VersionOfApi) {
|
||||
Given("We have a user with CanGetCacheInfo entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetCacheInfo.toString)
|
||||
|
||||
When("We make a request v6.0.0 with proper role")
|
||||
val request = (v6_0_0_Request / "system" / "cache" / "info").GET <@ (user1)
|
||||
val response = makeGetRequest(request)
|
||||
|
||||
Then("We should get a 200")
|
||||
response.code should equal(200)
|
||||
|
||||
And("The response should have the correct structure")
|
||||
val cacheInfo = response.body.extract[CacheInfoJsonV600]
|
||||
cacheInfo.namespaces should not be null
|
||||
cacheInfo.total_keys should be >= 0
|
||||
cacheInfo.redis_available shouldBe a[Boolean]
|
||||
|
||||
And("Each namespace should have valid data")
|
||||
cacheInfo.namespaces.foreach { namespace =>
|
||||
namespace.namespace_id should not be empty
|
||||
namespace.prefix should not be empty
|
||||
namespace.current_version should be > 0L
|
||||
namespace.key_count should be >= 0
|
||||
namespace.description should not be empty
|
||||
namespace.category should not be empty
|
||||
namespace.storage_location should not be empty
|
||||
namespace.storage_location should (equal("redis") or equal("memory") or equal("both") or equal("unknown"))
|
||||
namespace.ttl_info should not be empty
|
||||
namespace.ttl_info shouldBe a[String]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================================================
|
||||
// POST /management/cache/namespaces/invalidate - Invalidate Cache Namespace
|
||||
// ============================================================================================================
|
||||
|
||||
feature(s"test $ApiEndpoint3 version $VersionOfApi - Unauthorized access") {
|
||||
scenario("We call invalidateCacheNamespace without user credentials", ApiEndpoint3, VersionOfApi) {
|
||||
When("We make a request v6.0.0 without credentials")
|
||||
val request = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST
|
||||
val response = makePostRequest(request, write(InvalidateCacheNamespaceJsonV600("rd_localised")))
|
||||
Then("We should get a 401")
|
||||
response.code should equal(401)
|
||||
response.body.extract[ErrorMessage].message should equal(UserNotLoggedIn)
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint3 version $VersionOfApi - Missing role") {
|
||||
scenario("We call invalidateCacheNamespace without the CanInvalidateCacheNamespace role", ApiEndpoint3, VersionOfApi) {
|
||||
When("We make a request v6.0.0 without the required role")
|
||||
val request = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST <@ (user1)
|
||||
val response = makePostRequest(request, write(InvalidateCacheNamespaceJsonV600("rd_localised")))
|
||||
Then("We should get a 403")
|
||||
response.code should equal(403)
|
||||
And("error should be " + UserHasMissingRoles + CanInvalidateCacheNamespace)
|
||||
response.body.extract[ErrorMessage].message should equal(UserHasMissingRoles + CanInvalidateCacheNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint3 version $VersionOfApi - Invalid JSON format") {
|
||||
scenario("We call invalidateCacheNamespace with invalid JSON", ApiEndpoint3, VersionOfApi) {
|
||||
Given("We have a user with CanInvalidateCacheNamespace entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanInvalidateCacheNamespace.toString)
|
||||
|
||||
When("We make a request with invalid JSON")
|
||||
val request = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST <@ (user1)
|
||||
val response = makePostRequest(request, """{"invalid": "json"}""")
|
||||
|
||||
Then("We should get a 400")
|
||||
response.code should equal(400)
|
||||
And("error should be InvalidJsonFormat")
|
||||
response.body.extract[ErrorMessage].message should startWith(InvalidJsonFormat)
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint3 version $VersionOfApi - Invalid namespace_id") {
|
||||
scenario("We call invalidateCacheNamespace with non-existent namespace_id", ApiEndpoint3, VersionOfApi) {
|
||||
Given("We have a user with CanInvalidateCacheNamespace entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanInvalidateCacheNamespace.toString)
|
||||
|
||||
When("We make a request with invalid namespace_id")
|
||||
val request = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST <@ (user1)
|
||||
val response = makePostRequest(request, write(InvalidateCacheNamespaceJsonV600("invalid_namespace")))
|
||||
|
||||
Then("We should get a 400")
|
||||
response.code should equal(400)
|
||||
And("error should mention invalid namespace_id")
|
||||
val errorMessage = response.body.extract[ErrorMessage].message
|
||||
errorMessage should include("Invalid namespace_id")
|
||||
errorMessage should include("invalid_namespace")
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint3 version $VersionOfApi - Authorized access with valid namespace") {
|
||||
scenario("We call invalidateCacheNamespace with valid rd_localised namespace", ApiEndpoint3, VersionOfApi) {
|
||||
Given("We have a user with CanInvalidateCacheNamespace entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanInvalidateCacheNamespace.toString)
|
||||
|
||||
When("We make a request with valid namespace_id")
|
||||
val request = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST <@ (user1)
|
||||
val response = makePostRequest(request, write(InvalidateCacheNamespaceJsonV600("rd_localised")))
|
||||
|
||||
Then("We should get a 200")
|
||||
response.code should equal(200)
|
||||
|
||||
And("The response should have the correct structure")
|
||||
val result = response.body.extract[InvalidatedCacheNamespaceJsonV600]
|
||||
result.namespace_id should equal("rd_localised")
|
||||
result.old_version should be > 0L
|
||||
result.new_version should be > result.old_version
|
||||
result.new_version should equal(result.old_version + 1)
|
||||
result.status should equal("invalidated")
|
||||
}
|
||||
|
||||
scenario("We call invalidateCacheNamespace with valid connector namespace", ApiEndpoint3, VersionOfApi) {
|
||||
Given("We have a user with CanInvalidateCacheNamespace entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanInvalidateCacheNamespace.toString)
|
||||
|
||||
When("We make a request with connector namespace_id")
|
||||
val request = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST <@ (user1)
|
||||
val response = makePostRequest(request, write(InvalidateCacheNamespaceJsonV600("connector")))
|
||||
|
||||
Then("We should get a 200")
|
||||
response.code should equal(200)
|
||||
|
||||
And("The response should have the correct structure")
|
||||
val result = response.body.extract[InvalidatedCacheNamespaceJsonV600]
|
||||
result.namespace_id should equal("connector")
|
||||
result.old_version should be > 0L
|
||||
result.new_version should be > result.old_version
|
||||
result.status should equal("invalidated")
|
||||
}
|
||||
|
||||
scenario("We call invalidateCacheNamespace with valid abac_rule namespace", ApiEndpoint3, VersionOfApi) {
|
||||
Given("We have a user with CanInvalidateCacheNamespace entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanInvalidateCacheNamespace.toString)
|
||||
|
||||
When("We make a request with abac_rule namespace_id")
|
||||
val request = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST <@ (user1)
|
||||
val response = makePostRequest(request, write(InvalidateCacheNamespaceJsonV600("abac_rule")))
|
||||
|
||||
Then("We should get a 200")
|
||||
response.code should equal(200)
|
||||
|
||||
And("The response should have the correct structure")
|
||||
val result = response.body.extract[InvalidatedCacheNamespaceJsonV600]
|
||||
result.namespace_id should equal("abac_rule")
|
||||
result.status should equal("invalidated")
|
||||
}
|
||||
}
|
||||
|
||||
feature(s"test $ApiEndpoint3 version $VersionOfApi - Version increment validation") {
|
||||
scenario("We verify that cache version increments correctly on multiple invalidations", ApiEndpoint3, VersionOfApi) {
|
||||
Given("We have a user with CanInvalidateCacheNamespace entitlement")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanInvalidateCacheNamespace.toString)
|
||||
|
||||
When("We invalidate the same namespace twice")
|
||||
val request1 = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST <@ (user1)
|
||||
val response1 = makePostRequest(request1, write(InvalidateCacheNamespaceJsonV600("rd_dynamic")))
|
||||
|
||||
Then("First invalidation should succeed")
|
||||
response1.code should equal(200)
|
||||
val result1 = response1.body.extract[InvalidatedCacheNamespaceJsonV600]
|
||||
val firstNewVersion = result1.new_version
|
||||
|
||||
When("We invalidate again")
|
||||
val request2 = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST <@ (user1)
|
||||
val response2 = makePostRequest(request2, write(InvalidateCacheNamespaceJsonV600("rd_dynamic")))
|
||||
|
||||
Then("Second invalidation should succeed")
|
||||
response2.code should equal(200)
|
||||
val result2 = response2.body.extract[InvalidatedCacheNamespaceJsonV600]
|
||||
|
||||
And("Version should have incremented again")
|
||||
result2.old_version should equal(firstNewVersion)
|
||||
result2.new_version should equal(firstNewVersion + 1)
|
||||
result2.status should equal("invalidated")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================================================
|
||||
// Cross-endpoint test - Verify cache info updates after invalidation
|
||||
// ============================================================================================================
|
||||
|
||||
feature(s"Integration test - Cache endpoints interaction") {
|
||||
scenario("We verify cache info shows updated version after invalidation", ApiEndpoint2, ApiEndpoint3, VersionOfApi) {
|
||||
Given("We have a user with both CanGetCacheInfo and CanInvalidateCacheNamespace entitlements")
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanGetCacheInfo.toString)
|
||||
Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, CanInvalidateCacheNamespace.toString)
|
||||
|
||||
When("We get the initial cache info")
|
||||
val getRequest1 = (v6_0_0_Request / "system" / "cache" / "info").GET <@ (user1)
|
||||
val getResponse1 = makeGetRequest(getRequest1)
|
||||
getResponse1.code should equal(200)
|
||||
val cacheInfo1 = getResponse1.body.extract[CacheInfoJsonV600]
|
||||
|
||||
// Find the rd_static namespace (or any other valid namespace)
|
||||
val targetNamespace = "rd_static"
|
||||
val initialVersion = cacheInfo1.namespaces.find(_.namespace_id == targetNamespace).map(_.current_version)
|
||||
|
||||
When("We invalidate the namespace")
|
||||
val invalidateRequest = (v6_0_0_Request / "management" / "cache" / "namespaces" / "invalidate").POST <@ (user1)
|
||||
val invalidateResponse = makePostRequest(invalidateRequest, write(InvalidateCacheNamespaceJsonV600(targetNamespace)))
|
||||
invalidateResponse.code should equal(200)
|
||||
val invalidateResult = invalidateResponse.body.extract[InvalidatedCacheNamespaceJsonV600]
|
||||
|
||||
When("We get the cache info again")
|
||||
val getRequest2 = (v6_0_0_Request / "system" / "cache" / "info").GET <@ (user1)
|
||||
val getResponse2 = makeGetRequest(getRequest2)
|
||||
getResponse2.code should equal(200)
|
||||
val cacheInfo2 = getResponse2.body.extract[CacheInfoJsonV600]
|
||||
|
||||
Then("The namespace version should have been incremented")
|
||||
val updatedNamespace = cacheInfo2.namespaces.find(_.namespace_id == targetNamespace)
|
||||
updatedNamespace should not be None
|
||||
|
||||
if (initialVersion.isDefined) {
|
||||
updatedNamespace.get.current_version should be > initialVersion.get
|
||||
}
|
||||
updatedNamespace.get.current_version should equal(invalidateResult.new_version)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user