Merge pull request #2190 from constantine2nd/develop

A few changes
This commit is contained in:
Simon Redfern 2023-02-24 17:03:33 +01:00 committed by GitHub
commit 795d70ee86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 193 additions and 23 deletions

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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 applications access to a users account:
| - The user explicitly wishes to revoke the applications 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(

View File

@ -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")

View File

@ -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.")
}

View File

@ -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)