diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index bb3013b67..6f0a01708 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -594,7 +594,7 @@ class Boot extends MdcLoggable { LiftRules.explicitlyParsedSuffixes = Helpers.knownSuffixes &~ (Set("com")) val locale = I18NUtil.getDefaultLocale() - Locale.setDefault(locale) + // Locale.setDefault(locale) // TODO Explain why this line of code introduce weird side effects logger.info("Default Project Locale is :" + locale) // Cookie name diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index 9c3656608..b567031a0 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -964,7 +964,7 @@ object ResourceDocsAPIMethodsUtil extends MdcLoggable{ Some(resourceDocTags) } } - logger.info(s"tagsOption is $tags") + logger.debug(s"tagsOption is $tags") // So we can produce a reduced list of resource docs to prevent manual editing of swagger files. val rawPartialFunctionNames = S.param("functions") @@ -987,31 +987,31 @@ object ResourceDocsAPIMethodsUtil extends MdcLoggable{ Some(pfns) } } - logger.info(s"partialFunctionNames is $partialFunctionNames") + logger.debug(s"partialFunctionNames is $partialFunctionNames") // So we can produce a reduced list of resource docs to prevent manual editing of swagger files. val languageParam = for { x <- S.param("language").or(S.param(PARAM_LOCALE)) y <- stringToLanguageParam(x) } yield y - logger.info(s"languageParam is $languageParam") + logger.debug(s"languageParam is $languageParam") // So we can produce a reduced list of resource docs to prevent manual editing of swagger files. val contentParam = for { x <- S.param("content") y <- stringToContentParam(x) } yield y - logger.info(s"content is $contentParam") + logger.debug(s"content is $contentParam") val apiCollectionIdParam = for { x <- S.param("api-collection-id") } yield x - logger.info(s"apiCollectionIdParam is $apiCollectionIdParam") + logger.debug(s"apiCollectionIdParam is $apiCollectionIdParam") val cacheModifierParam = for { x <- S.param("cache-modifier") } yield x - logger.info(s"cacheModifierParam is $cacheModifierParam") + logger.debug(s"cacheModifierParam is $cacheModifierParam") (tags, partialFunctionNames, languageParam, contentParam, apiCollectionIdParam, cacheModifierParam) } @@ -1080,7 +1080,7 @@ so the caller must specify any required filtering by catalog explicitly. if (filteredResources4.length > 0 && resourcesToUse.length == 0) { - logger.info("tags filter reduced the list of resource docs to zero") + logger.debug("tags filter reduced the list of resource docs to zero") } resourcesToUse diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala index cd0a5c7fa..149402a25 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/SwaggerDefinitionsJSON.scala @@ -11,7 +11,7 @@ import code.api.dynamic.endpoint.helper.practise.PractiseEndpoint import code.api.util.APIUtil.{defaultJValue, _} import code.api.util.ApiRole._ import code.api.util.ExampleValue._ -import code.api.util.{ApiTrigger, ExampleValue} +import code.api.util.{APIUtil, ApiTrigger, ExampleValue} import code.api.v2_2_0.JSONFactory220.{AdapterImplementationJson, MessageDocJson, MessageDocsJson} import code.api.v3_0_0.JSONFactory300.createBranchJsonV300 import code.api.v3_0_0.custom.JSONFactoryCustom300 @@ -36,6 +36,7 @@ import com.openbankproject.commons.util.{ApiVersion, FieldNameApiVersions, Refle import net.liftweb.json import java.net.URLEncoder +import code.api.v5_1_0.CertificateInfoJsonV510 import code.endpointMapping.EndpointMappingCommons import scala.collection.immutable.List @@ -4154,6 +4155,15 @@ object SwaggerDefinitionsJSON { val oAuth2ServerJWKURIJson = OAuth2ServerJWKURIJson("https://www.googleapis.com/oauth2/v3/certs") val oAuth2ServerJwksUrisJson = OAuth2ServerJwksUrisJson(List(oAuth2ServerJWKURIJson)) + + val certificateInfoJsonV510 = CertificateInfoJsonV510( + subject_domain_name = "OID.2.5.4.41=VPN, EMAILADDRESS=admin@tesobe.com, CN=TESOBE CA, OU=TESOBE Operations, O=TESOBE, L=Berlin, ST=Berlin, C=DE", + issuer_domain_name = "CN=localhost, O=TESOBE GmbH, ST=Berlin, C=DE", + not_before = "2022-04-01T10:13:00.000Z", + not_after = "2032-04-01T10:13:00.000Z", + roles = None, + roles_info = Some("PEM Encoded Certificate does not contain PSD2 roles.") + ) val updateAccountRequestJsonV310 = UpdateAccountRequestJsonV310( label = "Label", diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index d9091a2ee..ec1c5a4bc 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -244,6 +244,17 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ } } } + + /** + * Purpose of this helper function is to get the Consent-JWT value from a Request Headers. + * @return the Consent-JWT value from a Request Header as a String + */ + def getConsentIdRequestHeaderValue(requestHeaders: List[HTTPParam]): Option[String] = { + requestHeaders.toSet.filter(_.name == RequestHeader.`Consent-Id`).toList match { + case x :: Nil => Some(x.values.mkString(", ")) + case _ => None + } + } /** * Purpose of this helper function is to get the PSD2-CERT value from a Request Headers. * @return the PSD2-CERT value from a Request Header as a String @@ -3648,12 +3659,11 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ `getPSD2-CERT`(cc.map(_.requestHeaders).getOrElse(Nil)) match { case Some(pem) => logger.debug("PSD2-CERT pem: " + pem) - val decodedPem = URLDecoder.decode(pem,"UTF-8") - val validatedPem = X509.validate(decodedPem) + val validatedPem = X509.validate(pem) logger.debug("validatedPem: " + validatedPem) validatedPem match { case Full(true) => - val hasServiceProvider = X509.extractPsd2Roles(decodedPem).map(_.exists(_ == serviceProvider)) + val hasServiceProvider = X509.extractPsd2Roles(pem).map(_.exists(_ == serviceProvider)) logger.debug("hasServiceProvider: " + hasServiceProvider) hasServiceProvider match { case Full(true) => Full(true) diff --git a/obp-api/src/main/scala/code/api/util/X509.scala b/obp-api/src/main/scala/code/api/util/X509.scala index 5211e9487..1d2d6d71c 100644 --- a/obp-api/src/main/scala/code/api/util/X509.scala +++ b/obp-api/src/main/scala/code/api/util/X509.scala @@ -5,6 +5,8 @@ import java.security.PublicKey import java.security.cert.{CertificateExpiredException, CertificateNotYetValidException, X509Certificate} import java.security.interfaces.{ECPublicKey, RSAPublicKey} +import code.api.v5_1_0.CertificateInfoJsonV510 +import code.util.Helper.MdcLoggable import com.github.dwickern.macros.NameOf import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jose.util.X509CertUtils @@ -13,7 +15,7 @@ import org.bouncycastle.asn1._ import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.qualified.QCStatement -object X509 { +object X509 extends MdcLoggable { object OID { lazy val role = "2.5.4.72" @@ -76,7 +78,7 @@ object X509 { psd2Roles += (psd2RoleSequence.getObjectAt(1).toASN1Primitive.toString) } } - org.scalameta.logger.elem(psd2Roles.toList) + logger.debug(psd2Roles.toList) psd2Roles.toList } @@ -201,5 +203,48 @@ object X509 { val rsaJWK = RSAKey.parse(cert) rsaJWK } + + + private def extractCertificateInfo(pem: String): Box[CertificateInfoJsonV510] = { + // Parse X.509 certificate + val cert: X509Certificate = X509CertUtils.parse(pem) + if (cert == null) { + // Parsing failed + Failure(ErrorMessages.X509ParsingFailed) + } else { + val subjectDN = cert.getSubjectDN().getName() + val issuerDN = cert.getIssuerDN().getName() + val notBefore = cert.getNotBefore() + val notAfter = cert.getNotAfter() + var roles: Option[List[String]] = None + var rolesInfo: Option[String] = None + try { + val qcstatements = extractQcStatements(cert) + val asn1encodable = extractPsd2QcStatements(qcstatements) + roles = Some(getPsd2Roles(asn1encodable: Array[ASN1Encodable])) + } + catch { + case _: Throwable => + Failure(ErrorMessages.X509ThereAreNoPsd2Roles) + rolesInfo = Some("PEM Encoded Certificate does not contain PSD2 roles.") + } + val result = CertificateInfoJsonV510( + subject_domain_name = subjectDN, + issuer_domain_name = issuerDN, + not_before = APIUtil.formatDate(notBefore), + not_after = APIUtil.formatDate(notAfter), + roles = roles, + roles_info = rolesInfo + ) + Full(result) + } + } + + def getCertificateInfo(pem: Option[String]): Box[CertificateInfoJsonV510] = { + pem match { + case Some(value) => extractCertificateInfo(value) + case None => Failure(ErrorMessages.X509CannotGetCertificate) + } + } } diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index 2edb5d91c..052a49fa7 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -7,7 +7,7 @@ import code.api.util.APIUtil._ import code.api.util.ApiRole._ import code.api.util.ApiTag._ import code.api.util.ErrorMessages.{$UserNotLoggedIn, BankNotFound, ConsentNotFound, InvalidJsonFormat, UnknownError, UserNotFoundByUserId, UserNotLoggedIn, _} -import code.api.util.{ApiRole, NewStyle} +import code.api.util.{APIUtil, ApiRole, NewStyle, X509} import code.api.util.NewStyle.HttpCode import code.api.v3_0_0.JSONFactory300.createAggregateMetricJson import code.api.v3_1_0.ConsentJsonV310 @@ -112,8 +112,8 @@ trait APIMethods510 { revokeConsentAtBank, implementedInApiVersion, nameOf(revokeConsentAtBank), - "GET", - "/banks/BANK_ID/consents/CONSENT_ID/revoke", + "DELETE", + "/banks/BANK_ID/consents/CONSENT_ID", "Revoke Consent at Bank", s""" |Revoke Consent specified by CONSENT_ID @@ -141,7 +141,7 @@ trait APIMethods510 { ) lazy val revokeConsentAtBank: OBPEndpoint = { - case "banks" :: BankId(bankId) :: "consents" :: consentId :: "revoke" :: Nil JsonGet _ => { + case "banks" :: BankId(bankId) :: "consents" :: consentId :: Nil JsonDelete _ => { cc => for { (Full(user), callContext) <- authenticatedAccess(cc) @@ -160,6 +160,91 @@ trait APIMethods510 { } } } + + staticResourceDocs += ResourceDoc( + selfRevokeConsent, + implementedInApiVersion, + nameOf(selfRevokeConsent), + "DELETE", + "/my/consent/current", + "Revoke Consent used in the Current Call", + s""" + |Revoke Consent specified by Consent-Id at Request Header + | + |There are a few reasons you might need to revoke an application’s access to a user’s account: + | - The user explicitly wishes to revoke the application’s access + | - You as the service provider have determined an application is compromised or malicious, and want to disable it + | - etc. + || + |OBP as a resource server stores access tokens in a database, then it is relatively easy to revoke some token that belongs to a particular user. + |The status of the token is changed to "REVOKED" so the next time the revoked client makes a request, their token will fail to validate. + | + |${authenticationRequiredMessage(true)} + | + """.stripMargin, + EmptyBody, + revokedConsentJsonV310, + List( + UserNotLoggedIn, + BankNotFound, + UnknownError + ), + List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2, apiTagNewStyle) + ) + lazy val selfRevokeConsent: OBPEndpoint = { + case "my" :: "consent" :: "current" :: Nil JsonDelete _ => { + cc => + for { + (Full(user), callContext) <- authenticatedAccess(cc) + consentId = getConsentIdRequestHeaderValue(cc.requestHeaders).getOrElse("") + _ <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { + unboxFullOrFail(_, callContext, ConsentNotFound) + } + consent <- Future(Consents.consentProvider.vend.revoke(consentId)) map { + i => connectorEmptyResponse(i, callContext) + } + } yield { + (ConsentJsonV310(consent.consentId, consent.jsonWebToken, consent.status), HttpCode.`200`(callContext)) + } + } + } + + + staticResourceDocs += ResourceDoc( + mtlsClientCertificateInfo, + implementedInApiVersion, + nameOf(mtlsClientCertificateInfo), + "GET", + "/my/mtls/certificate/current", + "Provide client's certificate info of a current call", + s""" + |Provide client's certificate info of a current call specified by PSD2-CERT value at Request Header + | + |${authenticationRequiredMessage(true)} + | + """.stripMargin, + EmptyBody, + certificateInfoJsonV510, + List( + UserNotLoggedIn, + BankNotFound, + UnknownError + ), + List(apiTagConsent, apiTagPSD2AIS, apiTagPsd2, apiTagNewStyle) + ) + lazy val mtlsClientCertificateInfo: OBPEndpoint = { + case "my" :: "mtls" :: "certificate" :: "current" :: Nil JsonGet _ => { + cc => + for { + (Full(_), callContext) <- authenticatedAccess(cc) + info <- Future(X509.getCertificateInfo(APIUtil.`getPSD2-CERT`(cc.requestHeaders))) map { + unboxFullOrFail(_, callContext, X509GeneralError) + } + } yield { + (info, HttpCode.`200`(callContext)) + } + } + } staticResourceDocs += ResourceDoc( diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index d9a8a5f22..430a987b7 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -47,6 +47,15 @@ case class APIInfoJsonV510( resource_docs_requires_role: Boolean ) +case class CertificateInfoJsonV510( + subject_domain_name: String, + issuer_domain_name: String, + not_before: String, + not_after: String, + roles: Option[List[String]], + roles_info: Option[String] = None + ) + object JSONFactory510 { def getApiInfoJSON(apiVersion : ApiVersion, apiVersionStatus: String) = { val organisation = APIUtil.getPropsValue("hosted_by.organisation", "TESOBE") diff --git a/obp-api/src/main/scala/code/scheduler/MetricsArchiveScheduler.scala b/obp-api/src/main/scala/code/scheduler/MetricsArchiveScheduler.scala index 105dbdac9..636281791 100644 --- a/obp-api/src/main/scala/code/scheduler/MetricsArchiveScheduler.scala +++ b/obp-api/src/main/scala/code/scheduler/MetricsArchiveScheduler.scala @@ -21,11 +21,13 @@ object MetricsArchiveScheduler extends MdcLoggable { private val oneDayInMillis: Long = 86400000 def start(intervalInSeconds: Long): Unit = { + logger.info("Hello from MetricsArchiveScheduler.start") scheduler.schedule( initialDelay = Duration(getMillisTillMidnight(), TimeUnit.MILLISECONDS), interval = Duration(intervalInSeconds, TimeUnit.SECONDS), runnable = new Runnable { def run(): Unit = { + logger.info("Hello from MetricsArchiveScheduler.start.run") conditionalDeleteMetricsRow() deleteOutdatedRowsFromMetricsArchive() } @@ -34,6 +36,7 @@ object MetricsArchiveScheduler extends MdcLoggable { } def deleteOutdatedRowsFromMetricsArchive() = { + logger.info("Hello from MetricsArchiveScheduler.deleteOutdatedRowsFromMetricsArchive") val currentTime = new Date() val defaultValue : Int = 365 * 3 val days = APIUtil.getPropsAsLongValue("retain_archive_metrics_days", defaultValue) match { @@ -43,9 +46,11 @@ object MetricsArchiveScheduler extends MdcLoggable { val someYearsAgo: Date = new Date(currentTime.getTime - (oneDayInMillis * days)) // Delete the outdated rows from the table "MetricsArchive" MetricArchive.bulkDelete_!!(By_<=(MetricArchive.date, someYearsAgo)) + logger.info("Bye from MetricsArchiveScheduler.deleteOutdatedRowsFromMetricsArchive") } def conditionalDeleteMetricsRow() = { + logger.info("Hello from MetricsArchiveScheduler.conditionalDeleteMetricsRow") val currentTime = new Date() val days = APIUtil.getPropsAsLongValue("retain_metrics_days", 367) match { case days if days > 59 => days @@ -53,11 +58,16 @@ object MetricsArchiveScheduler extends MdcLoggable { } val someDaysAgo: Date = new Date(currentTime.getTime - (oneDayInMillis * days)) // Get the data from the table "Metric" older than specified by retain_metrics_days + logger.info("MetricsArchiveScheduler.conditionalDeleteMetricsRow says before candidateMetricRowsToMove val") val candidateMetricRowsToMove = APIMetrics.apiMetrics.vend.getAllMetrics(List(OBPToDate(someDaysAgo))) + logger.info("MetricsArchiveScheduler.conditionalDeleteMetricsRow says after candidateMetricRowsToMove val") + logger.info(s"Number of rows: ${candidateMetricRowsToMove.length}") candidateMetricRowsToMove map { i => - // and copy it to the table "MetricsArchive" + // and copy it to the table "MetricArchive" copyRowToMetricsArchive(i) } + logger.info("MetricsArchiveScheduler.conditionalDeleteMetricsRow says after coping all rows") + logger.info("MetricsArchiveScheduler.conditionalDeleteMetricsRow says before maybeDeletedRows val") val maybeDeletedRows: List[(Boolean, Long)] = candidateMetricRowsToMove map { i => // and delete it after successful coping MetricArchive.find(By(MetricArchive.metricId, i.getMetricId())) match { @@ -65,6 +75,7 @@ object MetricsArchiveScheduler extends MdcLoggable { case _ => (false, i.getMetricId()) } } + logger.info("MetricsArchiveScheduler.conditionalDeleteMetricsRow says after maybeDeletedRows val") maybeDeletedRows.filter(_._1 == false).map { i => logger.warn(s"Row with primary key ${i._2} of the table Metric is not successfully copied.") } diff --git a/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala index 93e9557a8..6e1283edd 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/ConsentsTest.scala @@ -80,12 +80,12 @@ class ConsentsTest extends V510ServerSetup with PropsReset{ def getConsentRequestUrl(requestId:String) = (v5_1_0_Request / "consumer"/ "consent-requests"/requestId).GET<@(user1) def createConsentByConsentRequestIdEmail(requestId:String) = (v5_1_0_Request / "consumer"/ "consent-requests"/requestId/"EMAIL"/"consents").POST<@(user1) def getConsentByRequestIdUrl(requestId:String) = (v5_1_0_Request / "consumer"/ "consent-requests"/requestId/"consents").GET<@(user1) - def revokeConsentUrl(consentId: String) = v5_1_0_Request / "banks" / bankId / "consents" / consentId / "revoke" + def revokeConsentUrl(consentId: String) = (v5_1_0_Request / "banks" / bankId / "consents" / consentId).DELETE feature(s"test $ApiEndpoint6 version $VersionOfApi - Unauthorized access") { scenario("We will call the endpoint without user credentials", ApiEndpoint6, VersionOfApi) { When(s"We make a request $ApiEndpoint6") - val response510 = makeGetRequest(revokeConsentUrl("whatever")) + val response510 = makeDeleteRequest(revokeConsentUrl("whatever")) Then("We should get a 401") response510.code should equal(401) response510.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) @@ -94,7 +94,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{ feature(s"test $ApiEndpoint6 version $VersionOfApi - Authorized access") { scenario("We will call the endpoint without user credentials", ApiEndpoint6, VersionOfApi) { When(s"We make a request $ApiEndpoint1") - val response510 = makeGetRequest(revokeConsentUrl("whatever")<@(user1)) + val response510 = makeDeleteRequest(revokeConsentUrl("whatever")<@(user1)) Then("We should get a 403") response510.code should equal(403) response510.body.extract[ErrorMessage].message contains (UserHasMissingRoles + CanRevokeConsentAtBank) should be (true) @@ -177,7 +177,7 @@ class ConsentsTest extends V510ServerSetup with PropsReset{ // Revoke consent Entitlement.entitlement.vend.addEntitlement(bankId, resourceUser1.userId, CanRevokeConsentAtBank.toString) - val response510 = makeGetRequest(revokeConsentUrl(getConsentByRequestResponseJson.consent_id)<@(user1)) + val response510 = makeDeleteRequest(revokeConsentUrl(getConsentByRequestResponseJson.consent_id)<@(user1)) Then("We should get a 200") response510.code should equal(200)