From 86d12294a6c233608f012c2074d1c43090ec0ac5 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 15 Jul 2024 11:07:27 +0200 Subject: [PATCH 1/6] refactor/added more long to consent logic --- .../scala/code/api/util/ConsentUtil.scala | 26 ++++++++++++------- .../main/scala/code/api/util/JwtUtil.scala | 10 +++++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 479d05b54e..42f9edb67f 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -137,7 +137,10 @@ object Consent extends MdcLoggable { } private def verifyHmacSignedJwt(jwtToken: String, c: MappedConsent): Boolean = { - JwtUtil.verifyHmacSignedJwt(jwtToken, c.secret) + logger.debug(s"code.api.util.Consent.verifyHmacSignedJwt beginning:: jwtToken($jwtToken), MappedConsent($c)") + val result = JwtUtil.verifyHmacSignedJwt(jwtToken, c.secret) + logger.debug(s"code.api.util.Consent.verifyHmacSignedJwt result:: result($result)") + result } private def checkConsumerIsActiveAndMatched(consent: ConsentJWT, callContext: CallContext): Box[Boolean] = { @@ -180,7 +183,10 @@ object Consent extends MdcLoggable { } private def checkConsent(consent: ConsentJWT, consentIdAsJwt: String, callContext: CallContext): Box[Boolean] = { - Consents.consentProvider.vend.getConsentByConsentId(consent.jti) match { + logger.debug(s"code.api.util.Consent.checkConsent beginning: consent($consent), consentIdAsJwt($consentIdAsJwt)") + val consentBox = Consents.consentProvider.vend.getConsentByConsentId(consent.jti) + logger.debug(s"code.api.util.Consent.checkConsent.consentBox: consentBox($consentBox)") + consentBox match { case Full(c) if c.mStatus == ConsentStatus.ACCEPTED.toString | c.mStatus == ConsentStatus.VALID.toString => verifyHmacSignedJwt(consentIdAsJwt, c) match { case true => @@ -316,9 +322,9 @@ object Consent extends MdcLoggable { JwtUtil.getSignedPayloadAsJson(consentIdAsJwt) match { case Full(jsonAsString) => try { - logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") + logger.debug(s"applyConsentRulesCommonOldStyle.getSignedPayloadAsJson.Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] - logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") + logger.debug(s"applyConsentRulesCommonOldStyle.getSignedPayloadAsJson.End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") checkConsent(consent, consentIdAsJwt, calContext) match { // Check is it Consent-JWT expired case (Full(true)) => // OK applyConsentRules(consent) @@ -373,9 +379,9 @@ object Consent extends MdcLoggable { JwtUtil.getSignedPayloadAsJson(consentAsJwt) match { case Full(jsonAsString) => try { - logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") + logger.debug(s"applyConsentRulesCommon.Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] - logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") + logger.debug(s"applyConsentRulesCommon.End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") // Set Consumer into Call Context val consumer = getCurrentConsumerViaMtls(callContext) val updatedCallContext = callContext.copy(consumer = consumer) @@ -494,10 +500,12 @@ object Consent extends MdcLoggable { JwtUtil.getSignedPayloadAsJson(storedConsent.jsonWebToken) match { case Full(jsonAsString) => try { - logger.debug(s"Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") + logger.debug(s"applyBerlinGroupConsentRulesCommon.Start of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $jsonAsString") val consent = net.liftweb.json.parse(jsonAsString).extract[ConsentJWT] - logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") - checkConsent(consent, storedConsent.jsonWebToken, updatedCallContext) match { // Check is it Consent-JWT expired + logger.debug(s"applyBerlinGroupConsentRulesCommon.End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT]: $consent") + val consentBox = checkConsent(consent, storedConsent.jsonWebToken, updatedCallContext) + logger.debug(s"End of net.liftweb.json.parse(jsonAsString).extract[ConsentJWT].checkConsent.consentBox: $consent") + consentBox match { // Check is it Consent-JWT expired case (Full(true)) => // OK // Update MappedConsent.usesSoFarTodayCounter field Consents.consentProvider.vend.updateBerlinGroupConsent(consentId, currentCounterState + 1) diff --git a/obp-api/src/main/scala/code/api/util/JwtUtil.scala b/obp-api/src/main/scala/code/api/util/JwtUtil.scala index 96ec3c33b2..80d3c43119 100644 --- a/obp-api/src/main/scala/code/api/util/JwtUtil.scala +++ b/obp-api/src/main/scala/code/api/util/JwtUtil.scala @@ -3,7 +3,6 @@ package code.api.util import java.net.{URI, URL} import java.nio.file.{Files, Paths} import java.text.ParseException - import code.api.util.RSAUtil.logger import code.util.Helper.MdcLoggable import com.nimbusds.jose.JWSAlgorithm @@ -15,6 +14,7 @@ import com.nimbusds.jose.util.{DefaultResourceRetriever, JSONObjectUtils} import com.nimbusds.jwt.proc.{BadJWTException, DefaultJWTProcessor} import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT} import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet +import dispatch.Future import net.liftweb.common.{Box, Empty, Failure, Full} object JwtUtil extends MdcLoggable { @@ -58,9 +58,14 @@ object JwtUtil extends MdcLoggable { * @return True or False */ def verifyHmacSignedJwt(jwtToken: String, sharedSecret: String): Boolean = { + logger.debug(s"code.api.util.JwtUtil.verifyHmacSignedJwt beginning:: jwtToken($jwtToken), sharedSecret($sharedSecret)") val signedJWT = SignedJWT.parse(jwtToken) val verifier = new MACVerifier(sharedSecret) - signedJWT.verify(verifier) + logger.debug(s"code.api.util.JwtUtil.verifyHmacSignedJwt beginning:: signedJWT($signedJWT)") + logger.debug(s"code.api.util.JwtUtil.verifyHmacSignedJwt beginning:: verifier($verifier)") + val result = signedJWT.verify(verifier) + logger.debug(s"code.api.util.JwtUtil.verifyHmacSignedJwt result:: result($verifier)") + result } /** @@ -287,6 +292,7 @@ object JwtUtil extends MdcLoggable { val idToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNmMDIyYTQ5ZTk3ODYxNDhhZDBlMzc5Y2M4NTQ4NDRlMzZjM2VkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IkQ0VlZTSThXXzBXSC1QM1o5TW9NSEEiLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUNIaTNyY0lDel9Kemk5UEdnY3RrVzRzRzdWQmtFV2d2QS9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTUyMzc3ODgwLCJleHAiOjE1NTIzODE0ODB9.g2gIxUPT2zFmeTpbeeU4t0vmzrwgbKJSSQ_V33e9iWx63aDSHreGOwAMn6bPlI7b3DXB6Kjzx_6OoijoEsyoUHdJ4Pa5Ds611KKgBKDL0ztqKAtcLFE66kiHtUSnZyFUiYykzE6uGcluBaeXVQOkZqpeXEwhUVbUZSkM0QZ1l2DoOnnJB3rsNsoTBVnIYfQDZR8huxNCb9gjrYTzvtjifYG8uJ7FWMndcTorlUUpd3TxFkxJvws8oD2Au564awNQsQymZ10ZVDQ-D_mImJo5EQDxRiCtwMRDP_UtIYI9AkBHbE_6hi8kbeop-gDpDsLvl1v4Wl_rFciRxPgXP07Xuw" println("validateIdToken: " + validateIdToken(idToken = idToken, remoteJWKSetUrl = "https://www.googleapis.com/oauth2/v3/certs").map("Logged in user: " + _.getSubject)) + } From bb81d86702f147677b2b4f43f3663509ce4bb0d7 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 15 Jul 2024 14:57:31 +0200 Subject: [PATCH 2/6] refactor/added the use object to log --- obp-api/src/main/scala/code/api/util/ConsentUtil.scala | 4 +++- obp-api/src/main/scala/code/users/LiftUsers.scala | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 42f9edb67f..956ca6c187 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -209,6 +209,7 @@ object Consent extends MdcLoggable { } private def getOrCreateUser(subject: String, issuer: String, consentId: Option[String], name: Option[String], email: Option[String]): Future[(Box[User], Boolean)] = { + logger.debug(s"getOrCreateUser(subject($subject), issuer($issuer), consentId($consentId), name($name), email($email))") Users.users.vend.getOrCreateUserByProviderIdFuture( provider = issuer, idGivenByProvider = subject, @@ -508,7 +509,8 @@ object Consent extends MdcLoggable { consentBox match { // Check is it Consent-JWT expired case (Full(true)) => // OK // Update MappedConsent.usesSoFarTodayCounter field - Consents.consentProvider.vend.updateBerlinGroupConsent(consentId, currentCounterState + 1) + val consentUpdatedBox = Consents.consentProvider.vend.updateBerlinGroupConsent(consentId, currentCounterState + 1) + logger.debug(s"applyBerlinGroupConsentRulesCommon.consentUpdatedBox: $consentUpdatedBox") applyConsentRules(consent) case failure@Failure(_, _, _) => // Handled errors Future(failure, Some(updatedCallContext)) diff --git a/obp-api/src/main/scala/code/users/LiftUsers.scala b/obp-api/src/main/scala/code/users/LiftUsers.scala index 585ee1d336..f5a21983d0 100644 --- a/obp-api/src/main/scala/code/users/LiftUsers.scala +++ b/obp-api/src/main/scala/code/users/LiftUsers.scala @@ -1,7 +1,8 @@ package code.users -import java.util.Date +import code.api.util.Consent.logger +import java.util.Date import code.api.util._ import code.entitlement.Entitlement import code.loginattempts.LoginAttempt.maxBadLoginAttempts @@ -70,7 +71,9 @@ object LiftUsers extends Users with MdcLoggable{ } def getOrCreateUserByProviderIdFuture(provider : String, idGivenByProvider : String, consentId: Option[String], name: Option[String], email: Option[String]) : Future[(Box[User], Boolean)] = { Future { - getOrCreateUserByProviderId(provider, idGivenByProvider,consentId, name, email) + val result = getOrCreateUserByProviderId(provider, idGivenByProvider, consentId, name, email) + logger.debug(s"getOrCreateUserByProviderId.result ($result)") + result } } From 1be62420ed097e15fa21058ec936b75bc933ead4 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 15 Jul 2024 15:13:15 +0200 Subject: [PATCH 3/6] refactor/added the use checkConsumerIsActiveAndMatched result to log --- .../src/main/scala/code/api/util/ConsentUtil.scala | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 956ca6c187..06da0d5aa3 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -185,8 +185,8 @@ object Consent extends MdcLoggable { private def checkConsent(consent: ConsentJWT, consentIdAsJwt: String, callContext: CallContext): Box[Boolean] = { logger.debug(s"code.api.util.Consent.checkConsent beginning: consent($consent), consentIdAsJwt($consentIdAsJwt)") val consentBox = Consents.consentProvider.vend.getConsentByConsentId(consent.jti) - logger.debug(s"code.api.util.Consent.checkConsent.consentBox: consentBox($consentBox)") - consentBox match { + logger.debug(s"code.api.util.Consent.checkConsent.getConsentByConsentId: consentBox($consentBox)") + val result = consentBox match { case Full(c) if c.mStatus == ConsentStatus.ACCEPTED.toString | c.mStatus == ConsentStatus.VALID.toString => verifyHmacSignedJwt(consentIdAsJwt, c) match { case true => @@ -196,7 +196,10 @@ object Consent extends MdcLoggable { case currentTimeInSeconds if currentTimeInSeconds > consent.exp => Failure(ErrorMessages.ConsentExpiredIssue) case _ => - checkConsumerIsActiveAndMatched(consent, callContext) + logger.debug(s"start code.api.util.Consent.checkConsent.checkConsumerIsActiveAndMatched(consent($consent))") + val result = checkConsumerIsActiveAndMatched(consent, callContext) + logger.debug(s"end code.api.util.Consent.checkConsent.checkConsumerIsActiveAndMatched: result($result)") + result } case false => Failure(ErrorMessages.ConsentVerificationIssue) @@ -206,6 +209,8 @@ object Consent extends MdcLoggable { case _ => Failure(ErrorMessages.ConsentNotFound) } + logger.debug(s"code.api.util.Consent.checkConsent.consentBox.result: result($result)") + result } private def getOrCreateUser(subject: String, issuer: String, consentId: Option[String], name: Option[String], email: Option[String]): Future[(Box[User], Boolean)] = { From edf7632f19f960f0a7172e609016b4b0e3ee6bdf Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 15 Jul 2024 15:29:28 +0200 Subject: [PATCH 4/6] refactor/added log for checkConsumerIsActiveAndMatched method --- obp-api/src/main/scala/code/api/util/ConsentUtil.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index 06da0d5aa3..cb1b04476d 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -144,7 +144,9 @@ object Consent extends MdcLoggable { } private def checkConsumerIsActiveAndMatched(consent: ConsentJWT, callContext: CallContext): Box[Boolean] = { - Consumers.consumers.vend.getConsumerByConsumerId(consent.aud) match { + val consumerBox = Consumers.consumers.vend.getConsumerByConsumerId(consent.aud) + logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.getConsumerByConsumerId consumerBox:: consumerBox($consumerBox)") + consumerBox match { case Full(consumerFromConsent) if consumerFromConsent.isActive.get == true => // Consumer is active val validationMetod = APIUtil.getPropsValue(nameOfProperty = "consumer_validation_method_for_consent", defaultValue = "CONSUMER_CERTIFICATE") if(validationMetod != "CONSUMER_CERTIFICATE" && Props.mode == Props.RunModes.Production) { @@ -153,6 +155,7 @@ object Consent extends MdcLoggable { validationMetod match { case "CONSUMER_KEY_VALUE" => val requestHeaderConsumerKey = getConsumerKey(callContext.requestHeaders) + logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.consumerBox.requestHeaderConsumerKey:: requestHeaderConsumerKey($requestHeaderConsumerKey)") requestHeaderConsumerKey match { case Some(reqHeaderConsumerKey) => if (reqHeaderConsumerKey == consumerFromConsent.key.get) @@ -163,6 +166,7 @@ object Consent extends MdcLoggable { } case "CONSUMER_CERTIFICATE" => val clientCert: String = APIUtil.`getPSD2-CERT`(callContext.requestHeaders).getOrElse(SecureRandomUtil.csprng.nextLong().toString) + logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.consumerBox clientCert:: clientCert($clientCert)") def removeBreakLines(input: String) = input .replace("\n", "") .replace("\r", "") From c81e439d7081761b2d7d8e118fe92bf6cf11d700 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Mon, 15 Jul 2024 19:30:42 +0200 Subject: [PATCH 5/6] refactor/added log for checkConsumerIsActiveAndMatched method -step2 --- obp-api/src/main/scala/code/api/util/ConsentUtil.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala index cb1b04476d..d1f2ceed1d 100644 --- a/obp-api/src/main/scala/code/api/util/ConsentUtil.scala +++ b/obp-api/src/main/scala/code/api/util/ConsentUtil.scala @@ -170,9 +170,13 @@ object Consent extends MdcLoggable { def removeBreakLines(input: String) = input .replace("\n", "") .replace("\r", "") - if (removeBreakLines(clientCert) == removeBreakLines(consumerFromConsent.clientCertificate.get)) + val certificate = consumerFromConsent.clientCertificate + logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.consumer.certificate:: certificate($certificate)") + logger.debug(s"code.api.util.Consent.checkConsumerIsActiveAndMatched.consumer.certificate.dbNotNull_?(${certificate.dbNotNull_?})") + if (certificate.dbNotNull_? && removeBreakLines(clientCert) == removeBreakLines(certificate.get)) { + logger.debug(s"certificate.dbNotNull_? && removeBreakLines(clientCert) == removeBreakLines(consumerFromConsent.clientCertificate.get) result == true") Full(true) // This consent can be used by current application - else // This consent can NOT be used by current application + } else // This consent can NOT be used by current application Failure(ErrorMessages.ConsentDoesNotMatchConsumer) case "NONE" => // This instance does not require validation method Full(true) From 0f71ffcbf232c5669b56f13b80e3db93546b8e34 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Tue, 16 Jul 2024 12:25:25 +0200 Subject: [PATCH 6/6] refactor/added log for checkConsumerIsActiveAndMatched method -step2 --- obp-api/src/main/scala/code/api/util/JwtUtil.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/JwtUtil.scala b/obp-api/src/main/scala/code/api/util/JwtUtil.scala index 80d3c43119..25e6303114 100644 --- a/obp-api/src/main/scala/code/api/util/JwtUtil.scala +++ b/obp-api/src/main/scala/code/api/util/JwtUtil.scala @@ -292,7 +292,6 @@ object JwtUtil extends MdcLoggable { val idToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNmMDIyYTQ5ZTk3ODYxNDhhZDBlMzc5Y2M4NTQ4NDRlMzZjM2VkYzEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0MDc0MDg3MTgxOTIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM5NjY4NTQyNDU3ODA4OTI5NTkiLCJlbWFpbCI6Im1hcmtvLm1pbGljLnNyYmlqYUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IkQ0VlZTSThXXzBXSC1QM1o5TW9NSEEiLCJuYW1lIjoiTWFya28gTWlsacSHIiwicGljdHVyZSI6Imh0dHBzOi8vbGg1Lmdvb2dsZXVzZXJjb250ZW50LmNvbS8tWGQ0NGhuSjZURG8vQUFBQUFBQUFBQUkvQUFBQUFBQUFBQUEvQUNIaTNyY0lDel9Kemk5UEdnY3RrVzRzRzdWQmtFV2d2QS9zOTYtYy9waG90by5qcGciLCJnaXZlbl9uYW1lIjoiTWFya28iLCJmYW1pbHlfbmFtZSI6Ik1pbGnEhyIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNTUyMzc3ODgwLCJleHAiOjE1NTIzODE0ODB9.g2gIxUPT2zFmeTpbeeU4t0vmzrwgbKJSSQ_V33e9iWx63aDSHreGOwAMn6bPlI7b3DXB6Kjzx_6OoijoEsyoUHdJ4Pa5Ds611KKgBKDL0ztqKAtcLFE66kiHtUSnZyFUiYykzE6uGcluBaeXVQOkZqpeXEwhUVbUZSkM0QZ1l2DoOnnJB3rsNsoTBVnIYfQDZR8huxNCb9gjrYTzvtjifYG8uJ7FWMndcTorlUUpd3TxFkxJvws8oD2Au564awNQsQymZ10ZVDQ-D_mImJo5EQDxRiCtwMRDP_UtIYI9AkBHbE_6hi8kbeop-gDpDsLvl1v4Wl_rFciRxPgXP07Xuw" println("validateIdToken: " + validateIdToken(idToken = idToken, remoteJWKSetUrl = "https://www.googleapis.com/oauth2/v3/certs").map("Logged in user: " + _.getSubject)) - }