From eef9c4110eb59bedfb8090ca3b6b191ee4e38075 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Wed, 30 Sep 2026 17:53:52 +0200 Subject: [PATCH 1/2] Update metrics_stream.proto --- obp-api/src/main/protobuf/metrics_stream.proto | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/obp-api/src/main/protobuf/metrics_stream.proto b/obp-api/src/main/protobuf/metrics_stream.proto index a05a6a79d3..4b9354472c 100644 --- a/obp-api/src/main/protobuf/metrics_stream.proto +++ b/obp-api/src/main/protobuf/metrics_stream.proto @@ -46,6 +46,20 @@ message MetricEvent { // Reference id of the consent (if any) that authorised the request. // Mirrors MetricJsonV600.consent_reference_id (REST v6.0.0+). string consent_reference_id = 18; + // The hops the request passed through: the X-Forwarded-For chain it arrived + // with, followed by the TCP peer OBP-API saw. Matches MetricJsonV600.forwarded_for. + // source_ip (14) is the client address OBP-API decided on from that chain. + string forwarded_for = 19; + // Authentication scheme of the call, never the credential: "Consent", "OAuth2", + // "OAuth1", "DirectLogin", "GatewayLogin", "DAuth", "Anonymous" or "Other". + // Matches MetricJsonV600.auth_type. + string auth_type = 20; + // How the caller's certificate was established: "direct", "forwarded" or "none"; + // empty when the request carried no certificate. Matches MetricJsonV600.certificate_trust. + string certificate_trust = 21; + // The specifics behind certificate_trust: the forwarding proxy's subject for + // "forwarded", the rejection reason for "none". Matches MetricJsonV600.certificate_trust_detail. + string certificate_trust_detail = 22; } // Live tail of API metrics as they are written. From 1e8334974e0799665cd90909421c54691509c0c8 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Wed, 30 Sep 2026 17:55:20 +0200 Subject: [PATCH 2/2] Allowing SYS in Roles conditionaly + metrics forwardedFor --- .../main/scala/code/api/util/APIUtil.scala | 12 ++++ .../scala/code/api/util/WriteMetricUtil.scala | 12 ++-- .../scala/code/api/v2_0_0/Http4s200.scala | 2 +- .../scala/code/api/v3_0_0/Http4s300.scala | 7 +- .../scala/code/api/v7_0_0/Http4s700.scala | 6 +- .../MetricsStreamServiceImpl.scala | 8 ++- .../grpc/metricsstream/api/MetricEvent.scala | 60 +++++++++++++++-- .../api/MetricsStreamProto.scala | 4 ++ .../code/api/v2_0_0/EntitlementTests.scala | 22 +++++++ .../api/v3_0_0/EntitlementRequestsTest.scala | 9 +++ .../metricsstream/MetricEventFieldsTest.scala | 66 +++++++++++++++++++ 11 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 obp-api/src/test/scala/code/obp/grpc/metricsstream/MetricEventFieldsTest.scala 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 cabf606bc5..ff0b13822e 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -2288,6 +2288,18 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ def isConsentUser(userId: String): Boolean = Users.users.vend.getUserByUserId(userId).exists(_.isConsentUser) + /** + * This says whether an Entitlement or a Scope may be written at `bankId`. + * + * Three values are allowed: the empty bank id, where a system Role is held; SYS, the system space + * of Dynamic Entities, whose Roles are granted there although no Bank has that id; and the id of a + * Bank that exists, matched exactly, case included. A row at any other bank id would be one that no + * check ever reads. Every endpoint that grants a Role or a Scope, or records a request for one, asks + * this, so that SYS is accepted in all of them and not only in the versions written after it. + */ + def isBankIdWhereRolesCanBeHeld(bankId: String, callContext: Option[CallContext]): Boolean = + bankId.isEmpty || bankId == DYNAMIC_ENTITY_SYSTEM_LEVEL_BANK_ID || BankX(BankId(bankId), callContext).map(_._1).isDefined + def hasEntitlement(bankId: String, userId: String, apiRole: ApiRole): Boolean = apiRole match { case RoleCombination(roles) => roles.forall(hasEntitlement(bankId, userId, _)) case role => diff --git a/obp-api/src/main/scala/code/api/util/WriteMetricUtil.scala b/obp-api/src/main/scala/code/api/util/WriteMetricUtil.scala index bc1abd9678..8b6ccbf461 100644 --- a/obp-api/src/main/scala/code/api/util/WriteMetricUtil.scala +++ b/obp-api/src/main/scala/code/api/util/WriteMetricUtil.scala @@ -102,8 +102,8 @@ object WriteMetricUtil extends MdcLoggable { import fields._ publishMetricEvent(userId, cc.url, cc.startTime.getOrElse(null), duration, userName, appName, developerEmail, consumerId, implementedByPartialFunction, cc.implementedInVersion, cc.verb, - cc.httpCode, cc.correlationId, sourceIp, targetIp, cc.operationId.getOrElse(""), - cc.consentReferenceId.orNull, cc.certificateTrust.orNull, cc.certificateTrustDetail.orNull) + cc.httpCode, cc.correlationId, sourceIp, targetIp, forwardedFor, cc.operationId.getOrElse(""), + cc.consentReferenceId.orNull, cc.certificateTrust.orNull, cc.certificateTrustDetail.orNull, authType) } } @@ -206,10 +206,12 @@ object WriteMetricUtil extends MdcLoggable { correlationId: String, sourceIp: String, targetIp: String, + forwardedFor: String, operationId: String, consentReferenceId: String, certificateTrust: String, - certificateTrustDetail: String): Unit = { + certificateTrustDetail: String, + authType: String): Unit = { if (!MetricsEventBus.isEnabled) return try { implicit val fmts = metricFormats @@ -232,11 +234,13 @@ object WriteMetricUtil extends MdcLoggable { "correlation_id" -> Option(correlationId).getOrElse(""), "source_ip" -> Option(sourceIp).getOrElse(""), "target_ip" -> Option(targetIp).getOrElse(""), + "forwarded_for" -> Option(forwardedFor).getOrElse(""), "api_instance_id" -> code.api.Constant.ApiInstanceId, "operation_id" -> Option(operationId).getOrElse(""), "consent_reference_id" -> Option(consentReferenceId).getOrElse(""), "certificate_trust" -> Option(certificateTrust).getOrElse(""), - "certificate_trust_detail" -> Option(certificateTrustDetail).getOrElse("") + "certificate_trust_detail" -> Option(certificateTrustDetail).getOrElse(""), + "auth_type" -> Option(authType).getOrElse("") )) MetricsEventBus.publish(payload) } catch { diff --git a/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala b/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala index cd5e04eafb..b4b6e63cfd 100644 --- a/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala +++ b/obp-api/src/main/scala/code/api/v2_0_0/Http4s200.scala @@ -1262,7 +1262,7 @@ object Http4s200 { APIUtil.hasAtLeastOneEntitlement(body.bank_id, user.userId, requiredEntitlements) } _ <- code.util.Helper.booleanToFuture(BankNotFound, cc = cc2) { - body.bank_id.isEmpty || BankX(BankId(body.bank_id), cc2).map(_._1).isDefined + APIUtil.isBankIdWhereRolesCanBeHeld(body.bank_id, cc2) } _ <- code.util.Helper.booleanToFuture(EntitlementAlreadyExists, cc = cc2) { !hasEntitlement(body.bank_id, userId, role) diff --git a/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala b/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala index a92f29a725..0a4c35341b 100644 --- a/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala +++ b/obp-api/src/main/scala/code/api/v3_0_0/Http4s300.scala @@ -1655,8 +1655,9 @@ object Http4s300 { case req @ POST -> `prefixPath` / "entitlement-requests" => EndpointHelpers.withUserAndBodyCreated[CreateEntitlementRequestJSON, EntitlementRequestJSON](req) { (user, body, cc) => for { - _ <- if (body.bank_id.isEmpty) Future.successful(()) - else NewStyle.function.getBank(BankId(body.bank_id), Some(cc)).map(_ => ()) + _ <- code.util.Helper.booleanToFuture(s"$BankNotFound Current BankId is ${body.bank_id}", failCode = 404, cc = Some(cc)) { + APIUtil.isBankIdWhereRolesCanBeHeld(body.bank_id, Some(cc)) + } _ <- code.util.Helper.booleanToFuture( IncorrectRoleName + body.role_name + ". Possible roles are " + ApiRole.availableRoles.sorted.mkString(", "), cc = Some(cc)) { availableRoles.exists(_ == body.role_name) } @@ -2088,7 +2089,7 @@ object Http4s300 { allowedEntitlementsTxt = s"$UserHasMissingRoles ${allowedEntitlements.mkString(", ")}!" _ <- NewStyle.function.hasAtLeastOneEntitlement(allowedEntitlementsTxt)(body.bank_id, user.userId, allowedEntitlements, Some(cc)) _ <- code.util.Helper.booleanToFuture(BankNotFound, cc = Some(cc)) { - body.bank_id.nonEmpty == false || BankX(BankId(body.bank_id), Some(cc)).map(_._1).isDefined + APIUtil.isBankIdWhereRolesCanBeHeld(body.bank_id, Some(cc)) } _ <- code.util.Helper.booleanToFuture(EntitlementAlreadyExists, cc = Some(cc)) { !hasScope(body.bank_id, consumerIdStr, role) diff --git a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala index e9df03cf73..a96b026b97 100644 --- a/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala +++ b/obp-api/src/main/scala/code/api/v7_0_0/Http4s700.scala @@ -544,8 +544,7 @@ object Http4s700 { // Bank ids are matched exactly, case included: a grant at a bank id naming no bank is a // row no check will ever read. SYS is the system space of Dynamic Entities, not a bank. _ <- Helper.booleanToFuture(failMsg = BankNotFound, failCode = 404, cc = Some(cc)) { - body.bank_id.isEmpty || body.bank_id == code.api.Constant.DYNAMIC_ENTITY_SYSTEM_LEVEL_BANK_ID || - code.model.BankX(BankId(body.bank_id), Some(cc)).map(_._1).isDefined + APIUtil.isBankIdWhereRolesCanBeHeld(body.bank_id, Some(cc)) } _ <- Helper.booleanToFuture(failMsg = EntitlementAlreadyExists, failCode = 409, cc = Some(cc))( !hasEntitlement(body.bank_id, userId, role)) @@ -881,8 +880,7 @@ object Http4s700 { APIUtil.hasAtLeastOneEntitlement(body.bank_id, user.userId, grantingRoles) } _ <- Helper.booleanToFuture(failMsg = BankNotFound, failCode = 404, cc = Some(cc)) { - body.bank_id.isEmpty || body.bank_id == code.api.Constant.DYNAMIC_ENTITY_SYSTEM_LEVEL_BANK_ID || - code.model.BankX(BankId(body.bank_id), Some(cc)).map(_._1).isDefined + APIUtil.isBankIdWhereRolesCanBeHeld(body.bank_id, Some(cc)) } _ <- Helper.booleanToFuture(failMsg = EntitlementAlreadyExists, failCode = 409, cc = Some(cc)) { !APIUtil.hasScope(body.bank_id, consumer.id.get.toString, role) diff --git a/obp-api/src/main/scala/code/obp/grpc/metricsstream/MetricsStreamServiceImpl.scala b/obp-api/src/main/scala/code/obp/grpc/metricsstream/MetricsStreamServiceImpl.scala index ed3324928f..3bab0d6186 100644 --- a/obp-api/src/main/scala/code/obp/grpc/metricsstream/MetricsStreamServiceImpl.scala +++ b/obp-api/src/main/scala/code/obp/grpc/metricsstream/MetricsStreamServiceImpl.scala @@ -123,7 +123,7 @@ object MetricsStreamServiceImpl extends MetricsStreamServiceGrpc.MetricsStreamSe matchExact(req.consentReferenceId, (jv \ "consent_reference_id").extractOrElse[String]("")) } - private def jsonToMetricEvent(jv: JValue): MetricEvent = { + private[metricsstream] def jsonToMetricEvent(jv: JValue): MetricEvent = { MetricEvent( url = (jv \ "url").extractOrElse[String](""), date = (jv \ "date").extractOrElse[String](""), @@ -142,7 +142,11 @@ object MetricsStreamServiceImpl extends MetricsStreamServiceGrpc.MetricsStreamSe targetIp = (jv \ "target_ip").extractOrElse[String](""), apiInstanceId = (jv \ "api_instance_id").extractOrElse[String](""), operationId = (jv \ "operation_id").extractOrElse[String](""), - consentReferenceId = (jv \ "consent_reference_id").extractOrElse[String]("") + consentReferenceId = (jv \ "consent_reference_id").extractOrElse[String](""), + forwardedFor = (jv \ "forwarded_for").extractOrElse[String](""), + authType = (jv \ "auth_type").extractOrElse[String](""), + certificateTrust = (jv \ "certificate_trust").extractOrElse[String](""), + certificateTrustDetail = (jv \ "certificate_trust_detail").extractOrElse[String]("") ) } } diff --git a/obp-api/src/main/scala/code/obp/grpc/metricsstream/api/MetricEvent.scala b/obp-api/src/main/scala/code/obp/grpc/metricsstream/api/MetricEvent.scala index b16d99a6c5..d51804cb63 100644 --- a/obp-api/src/main/scala/code/obp/grpc/metricsstream/api/MetricEvent.scala +++ b/obp-api/src/main/scala/code/obp/grpc/metricsstream/api/MetricEvent.scala @@ -51,7 +51,11 @@ final case class MetricEvent( targetIp: _root_.scala.Predef.String = "", apiInstanceId: _root_.scala.Predef.String = "", operationId: _root_.scala.Predef.String = "", - consentReferenceId: _root_.scala.Predef.String = "" + consentReferenceId: _root_.scala.Predef.String = "", + forwardedFor: _root_.scala.Predef.String = "", + authType: _root_.scala.Predef.String = "", + certificateTrust: _root_.scala.Predef.String = "", + certificateTrustDetail: _root_.scala.Predef.String = "" ) extends scalapb.GeneratedMessage with scalapb.Message[MetricEvent] with scalapb.lenses.Updatable[MetricEvent] { @transient private[this] var __serializedSizeCachedValue: _root_.scala.Int = 0 @@ -75,6 +79,10 @@ final case class MetricEvent( if (apiInstanceId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(16, apiInstanceId) } if (operationId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(17, operationId) } if (consentReferenceId != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(18, consentReferenceId) } + if (forwardedFor != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(19, forwardedFor) } + if (authType != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(20, authType) } + if (certificateTrust != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(21, certificateTrust) } + if (certificateTrustDetail != "") { __size += _root_.com.google.protobuf.CodedOutputStream.computeStringSize(22, certificateTrustDetail) } __size } final override def serializedSize: _root_.scala.Int = { @@ -104,6 +112,10 @@ final case class MetricEvent( { val __v = apiInstanceId; if (__v != "") _output__.writeString(16, __v) }; { val __v = operationId; if (__v != "") _output__.writeString(17, __v) }; { val __v = consentReferenceId; if (__v != "") _output__.writeString(18, __v) }; + { val __v = forwardedFor; if (__v != "") _output__.writeString(19, __v) }; + { val __v = authType; if (__v != "") _output__.writeString(20, __v) }; + { val __v = certificateTrust; if (__v != "") _output__.writeString(21, __v) }; + { val __v = certificateTrustDetail; if (__v != "") _output__.writeString(22, __v) }; } def mergeFrom(`_input__`: _root_.com.google.protobuf.CodedInputStream): code.obp.grpc.metricsstream.api.MetricEvent = { var __url = this.url @@ -124,6 +136,10 @@ final case class MetricEvent( var __apiInstanceId = this.apiInstanceId var __operationId = this.operationId var __consentReferenceId = this.consentReferenceId + var __forwardedFor = this.forwardedFor + var __authType = this.authType + var __certificateTrust = this.certificateTrust + var __certificateTrustDetail = this.certificateTrustDetail var _done__ = false while (!_done__) { val _tag__ = _input__.readTag() @@ -147,6 +163,10 @@ final case class MetricEvent( case 130 => __apiInstanceId = _input__.readString() case 138 => __operationId = _input__.readString() case 146 => __consentReferenceId = _input__.readString() + case 154 => __forwardedFor = _input__.readString() + case 162 => __authType = _input__.readString() + case 170 => __certificateTrust = _input__.readString() + case 178 => __certificateTrustDetail = _input__.readString() case tag => _input__.skipField(tag) } } @@ -168,7 +188,11 @@ final case class MetricEvent( targetIp = __targetIp, apiInstanceId = __apiInstanceId, operationId = __operationId, - consentReferenceId = __consentReferenceId + consentReferenceId = __consentReferenceId, + forwardedFor = __forwardedFor, + authType = __authType, + certificateTrust = __certificateTrust, + certificateTrustDetail = __certificateTrustDetail ) } def withUrl(__v: _root_.scala.Predef.String): MetricEvent = copy(url = __v) @@ -189,6 +213,10 @@ final case class MetricEvent( def withApiInstanceId(__v: _root_.scala.Predef.String): MetricEvent = copy(apiInstanceId = __v) def withOperationId(__v: _root_.scala.Predef.String): MetricEvent = copy(operationId = __v) def withConsentReferenceId(__v: _root_.scala.Predef.String): MetricEvent = copy(consentReferenceId = __v) + def withForwardedFor(__v: _root_.scala.Predef.String): MetricEvent = copy(forwardedFor = __v) + def withAuthType(__v: _root_.scala.Predef.String): MetricEvent = copy(authType = __v) + def withCertificateTrust(__v: _root_.scala.Predef.String): MetricEvent = copy(certificateTrust = __v) + def withCertificateTrustDetail(__v: _root_.scala.Predef.String): MetricEvent = copy(certificateTrustDetail = __v) def getFieldByNumber(__fieldNumber: _root_.scala.Int): scala.Any = { (__fieldNumber: @_root_.scala.unchecked) match { case 1 => { val __t = url; if (__t != "") __t else null } @@ -209,6 +237,10 @@ final case class MetricEvent( case 16 => { val __t = apiInstanceId; if (__t != "") __t else null } case 17 => { val __t = operationId; if (__t != "") __t else null } case 18 => { val __t = consentReferenceId; if (__t != "") __t else null } + case 19 => { val __t = forwardedFor; if (__t != "") __t else null } + case 20 => { val __t = authType; if (__t != "") __t else null } + case 21 => { val __t = certificateTrust; if (__t != "") __t else null } + case 22 => { val __t = certificateTrustDetail; if (__t != "") __t else null } } } def getField(__field: _root_.scalapb.descriptors.FieldDescriptor): _root_.scalapb.descriptors.PValue = { @@ -232,6 +264,10 @@ final case class MetricEvent( case 16 => _root_.scalapb.descriptors.PString(apiInstanceId) case 17 => _root_.scalapb.descriptors.PString(operationId) case 18 => _root_.scalapb.descriptors.PString(consentReferenceId) + case 19 => _root_.scalapb.descriptors.PString(forwardedFor) + case 20 => _root_.scalapb.descriptors.PString(authType) + case 21 => _root_.scalapb.descriptors.PString(certificateTrust) + case 22 => _root_.scalapb.descriptors.PString(certificateTrustDetail) } } def toProtoString: _root_.scala.Predef.String = _root_.scalapb.TextFormat.printToUnicodeString(this) @@ -261,7 +297,11 @@ object MetricEvent extends scalapb.GeneratedMessageCompanion[code.obp.grpc.metri __fieldsMap.getOrElse(__fields.get(14), "").asInstanceOf[_root_.scala.Predef.String], __fieldsMap.getOrElse(__fields.get(15), "").asInstanceOf[_root_.scala.Predef.String], __fieldsMap.getOrElse(__fields.get(16), "").asInstanceOf[_root_.scala.Predef.String], - __fieldsMap.getOrElse(__fields.get(17), "").asInstanceOf[_root_.scala.Predef.String] + __fieldsMap.getOrElse(__fields.get(17), "").asInstanceOf[_root_.scala.Predef.String], + __fieldsMap.getOrElse(__fields.get(18), "").asInstanceOf[_root_.scala.Predef.String], + __fieldsMap.getOrElse(__fields.get(19), "").asInstanceOf[_root_.scala.Predef.String], + __fieldsMap.getOrElse(__fields.get(20), "").asInstanceOf[_root_.scala.Predef.String], + __fieldsMap.getOrElse(__fields.get(21), "").asInstanceOf[_root_.scala.Predef.String] ) } implicit def messageReads: _root_.scalapb.descriptors.Reads[code.obp.grpc.metricsstream.api.MetricEvent] = _root_.scalapb.descriptors.Reads{ @@ -285,7 +325,11 @@ object MetricEvent extends scalapb.GeneratedMessageCompanion[code.obp.grpc.metri __fieldsMap.get(scalaDescriptor.findFieldByNumber(15).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""), __fieldsMap.get(scalaDescriptor.findFieldByNumber(16).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""), __fieldsMap.get(scalaDescriptor.findFieldByNumber(17).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""), - __fieldsMap.get(scalaDescriptor.findFieldByNumber(18).get).map(_.as[_root_.scala.Predef.String]).getOrElse("") + __fieldsMap.get(scalaDescriptor.findFieldByNumber(18).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""), + __fieldsMap.get(scalaDescriptor.findFieldByNumber(19).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""), + __fieldsMap.get(scalaDescriptor.findFieldByNumber(20).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""), + __fieldsMap.get(scalaDescriptor.findFieldByNumber(21).get).map(_.as[_root_.scala.Predef.String]).getOrElse(""), + __fieldsMap.get(scalaDescriptor.findFieldByNumber(22).get).map(_.as[_root_.scala.Predef.String]).getOrElse("") ) case _ => throw new RuntimeException("Expected PMessage") } @@ -314,6 +358,10 @@ object MetricEvent extends scalapb.GeneratedMessageCompanion[code.obp.grpc.metri def apiInstanceId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.apiInstanceId)((c_, f_) => c_.copy(apiInstanceId = f_)) def operationId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.operationId)((c_, f_) => c_.copy(operationId = f_)) def consentReferenceId: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.consentReferenceId)((c_, f_) => c_.copy(consentReferenceId = f_)) + def forwardedFor: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.forwardedFor)((c_, f_) => c_.copy(forwardedFor = f_)) + def authType: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.authType)((c_, f_) => c_.copy(authType = f_)) + def certificateTrust: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.certificateTrust)((c_, f_) => c_.copy(certificateTrust = f_)) + def certificateTrustDetail: _root_.scalapb.lenses.Lens[UpperPB, _root_.scala.Predef.String] = field(_.certificateTrustDetail)((c_, f_) => c_.copy(certificateTrustDetail = f_)) } final val URL_FIELD_NUMBER = 1 final val DATE_FIELD_NUMBER = 2 @@ -333,4 +381,8 @@ object MetricEvent extends scalapb.GeneratedMessageCompanion[code.obp.grpc.metri final val API_INSTANCE_ID_FIELD_NUMBER = 16 final val OPERATION_ID_FIELD_NUMBER = 17 final val CONSENT_REFERENCE_ID_FIELD_NUMBER = 18 + final val FORWARDED_FOR_FIELD_NUMBER = 19 + final val AUTH_TYPE_FIELD_NUMBER = 20 + final val CERTIFICATE_TRUST_FIELD_NUMBER = 21 + final val CERTIFICATE_TRUST_DETAIL_FIELD_NUMBER = 22 } diff --git a/obp-api/src/main/scala/code/obp/grpc/metricsstream/api/MetricsStreamProto.scala b/obp-api/src/main/scala/code/obp/grpc/metricsstream/api/MetricsStreamProto.scala index 88a98659ec..bc21f42f46 100644 --- a/obp-api/src/main/scala/code/obp/grpc/metricsstream/api/MetricsStreamProto.scala +++ b/obp-api/src/main/scala/code/obp/grpc/metricsstream/api/MetricsStreamProto.scala @@ -73,6 +73,10 @@ object MetricsStreamProto { .addField(stringField("api_instance_id", 16)) .addField(stringField("operation_id", 17)) .addField(stringField("consent_reference_id", 18)) + .addField(stringField("forwarded_for", 19)) + .addField(stringField("auth_type", 20)) + .addField(stringField("certificate_trust", 21)) + .addField(stringField("certificate_trust_detail", 22)) ) // MetricsStreamService .addService(ServiceDescriptorProto.newBuilder() diff --git a/obp-api/src/test/scala/code/api/v2_0_0/EntitlementTests.scala b/obp-api/src/test/scala/code/api/v2_0_0/EntitlementTests.scala index a4c1c7ed66..43d3a39c6c 100644 --- a/obp-api/src/test/scala/code/api/v2_0_0/EntitlementTests.scala +++ b/obp-api/src/test/scala/code/api/v2_0_0/EntitlementTests.scala @@ -147,6 +147,28 @@ class EntitlementTests extends V200ServerSetup with DefaultUsers { responsePost3.code should equal(201) responsePost3.body.extract[EntitlementJSON].bank_id should equal(testBankId1.value) } + + // SYS is the system space of Dynamic Entities, where their Roles are granted although no Bank has + // that id. API Manager accepts an Entitlement Request by calling this endpoint at v6.0.0, which + // cascades to this v2.0.0 handler, so it has to accept SYS as v7.0.0 does. + scenario("We try to create entitlement - addEntitlement at SYS, the system space of Dynamic Entities") { + Entitlement.entitlement.vend.addEntitlement("", resourceUser1.userId, ApiRole.canCreateEntitlementAtAnyBank.toString) + val requestBody = SwaggerDefinitionsJSON.createEntitlementJSON.copy( + bank_id = code.api.Constant.DYNAMIC_ENTITY_SYSTEM_LEVEL_BANK_ID, role_name = ApiRole.canGetDynamicEntityDefinitions.toString) + + When("the Role is granted at SYS through the v6.0.0 URL, as API Manager does") + val requestPost = (baseRequest / "obp" / "v6.0.0" / "users" / resourceUser2.userId / "entitlements").POST <@ (user1) + val responsePost = makePostRequest(requestPost, write(requestBody)) + + Then("it is granted, not refused as an unknown bank") + responsePost.code should equal(201) + responsePost.body.extract[EntitlementJSON].bank_id should equal(code.api.Constant.DYNAMIC_ENTITY_SYSTEM_LEVEL_BANK_ID) + + And("a bank id that is neither SYS nor a Bank is still refused") + val unknownBank = makePostRequest(requestPost, write(requestBody.copy(bank_id = "no-such-bank"))) + unknownBank.code should equal(400) + unknownBank.body.toString contains (extractErrorMessageCode(BankNotFound)) should be (true) + } } diff --git a/obp-api/src/test/scala/code/api/v3_0_0/EntitlementRequestsTest.scala b/obp-api/src/test/scala/code/api/v3_0_0/EntitlementRequestsTest.scala index 87c72ea15a..3e90ef9f3a 100644 --- a/obp-api/src/test/scala/code/api/v3_0_0/EntitlementRequestsTest.scala +++ b/obp-api/src/test/scala/code/api/v3_0_0/EntitlementRequestsTest.scala @@ -114,6 +114,15 @@ class EntitlementRequestsTest extends V300ServerSetup with DefaultUsers { response300.body.toString contains EntitlementIsSystemRole should be (true) } + scenario("create entitlement request at SYS, the system space of Dynamic Entities", VersionOfApi, ApiEndpoint1) { + When("We request a Dynamic Entity Role at SYS, where such Roles are granted although no Bank has that id") + val postJson = s"""{"bank_id":"${code.api.Constant.DYNAMIC_ENTITY_SYSTEM_LEVEL_BANK_ID}", "role_name":"CanGetDynamicEntityDefinitions"}""" + val request300 = (v3_0Request / "entitlement-requests").POST <@(user1) + val response300 = makePostRequest(request300, postJson) + Then("We should get a 201, not a 404 for an unknown bank") + response300.code should equal(201) + } + scenario("create entitlement request- successfully", VersionOfApi, ApiEndpoint1) { When("We make a request v3.0.0") val postJson = s"""{"bank_id":"${testBankId1.value}", "role_name":"CanCreateBankLevelEndpointTag"}""" diff --git a/obp-api/src/test/scala/code/obp/grpc/metricsstream/MetricEventFieldsTest.scala b/obp-api/src/test/scala/code/obp/grpc/metricsstream/MetricEventFieldsTest.scala new file mode 100644 index 0000000000..6de7ed8f05 --- /dev/null +++ b/obp-api/src/test/scala/code/obp/grpc/metricsstream/MetricEventFieldsTest.scala @@ -0,0 +1,66 @@ +package code.obp.grpc.metricsstream + +import code.obp.grpc.metricsstream.api.{MetricEvent, MetricsStreamProto} +import code.setup.ServerSetup +import org.json4s.native.JsonMethods.parse + +/** + * This suite checks the fields that the live metrics stream carries beyond the original 18: + * forwarded_for, auth_type, certificate_trust and certificate_trust_detail. MetricEvent and its + * descriptor are written by hand (no protoc plugin runs in the build), so a field number or name + * that disagrees between them, or a field left out of the wire format, would not be caught by the + * compiler. The suite also checks that the JSON payload WriteMetricUtil publishes is read into + * those fields. + */ +class MetricEventFieldsTest extends ServerSetup { + + private val newFields = List( + 19 -> "forwarded_for", + 20 -> "auth_type", + 21 -> "certificate_trust", + 22 -> "certificate_trust_detail" + ) + + private val event = MetricEvent( + url = "/obp/v6.0.0/banks", + sourceIp = "203.0.113.9", + consentReferenceId = "consent-1", + forwardedFor = "203.0.113.9, 10.0.0.2, 10.0.0.3", + authType = "OAuth2", + certificateTrust = "forwarded", + certificateTrustDetail = "CN=proxy,O=Example" + ) + + feature("MetricEvent fields 19 to 22") { + + scenario("the descriptor names them with the numbers the proto file gives them") { + val descriptor = MetricsStreamProto.javaDescriptor.findMessageTypeByName("MetricEvent") + newFields.foreach { case (number, name) => + descriptor.findFieldByNumber(number).getName shouldBe name + } + } + + scenario("they survive a round trip through the wire format") { + MetricEvent.parseFrom(event.toByteArray) shouldBe event + } + + scenario("getFieldByNumber returns each of them") { + event.getFieldByNumber(19) shouldBe "203.0.113.9, 10.0.0.2, 10.0.0.3" + event.getFieldByNumber(20) shouldBe "OAuth2" + event.getFieldByNumber(21) shouldBe "forwarded" + event.getFieldByNumber(22) shouldBe "CN=proxy,O=Example" + } + + scenario("the published JSON payload is read into them") { + val payload = parse( + """{"url":"/obp/v6.0.0/banks","source_ip":"203.0.113.9","forwarded_for":"203.0.113.9, 10.0.0.2", + |"auth_type":"Consent","certificate_trust":"direct","certificate_trust_detail":""}""".stripMargin) + val fromPayload = MetricsStreamServiceImpl.jsonToMetricEvent(payload) + fromPayload.sourceIp shouldBe "203.0.113.9" + fromPayload.forwardedFor shouldBe "203.0.113.9, 10.0.0.2" + fromPayload.authType shouldBe "Consent" + fromPayload.certificateTrust shouldBe "direct" + fromPayload.certificateTrustDetail shouldBe "" + } + } +}