diff --git a/README.md b/README.md index e789fcf8..9d996d8b 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,10 @@ Now you can disable root by setting `api.root.enabled` to `false` in `/etc/horiz - detect if a pattern is updated with service that has userInput w/o default values, and give warning - Consider changing all creates to POST, and update (via put/patch) return codes to 200 +## Changes in 2.29.0 + +- Issue 342: Notification Framework Performance: Added agbot filters for nodemsgs, agbotmsgs, nodestatus, nodeagreements, and agbotagreements + ## Changes in 2.28.0 - Issue 358: Limited user role on `POST /org/{orgid}/search/nodes/error` API to only retrieve nodes self owned. diff --git a/src/main/resources/version.txt b/src/main/resources/version.txt index 67d9e1b3..67b680c8 100644 --- a/src/main/resources/version.txt +++ b/src/main/resources/version.txt @@ -1 +1 @@ -2.28.0 \ No newline at end of file +2.29.0 \ No newline at end of file diff --git a/src/main/scala/com/horizon/exchangeapi/OrgsRoutes.scala b/src/main/scala/com/horizon/exchangeapi/OrgsRoutes.scala index cbcb163e..c6415777 100644 --- a/src/main/scala/com/horizon/exchangeapi/OrgsRoutes.scala +++ b/src/main/scala/com/horizon/exchangeapi/OrgsRoutes.scala @@ -678,10 +678,14 @@ trait OrgsRoutes extends JacksonSupport with AuthenticationSupport { qFilter = qFilter.filter(u => (u.orgId === orgId) || (u.orgId =!= orgId && u.public === "true")).filter(u => (u.category === "node" && u.id === ident.getIdentity) || u.category =!= "node") case _: IAgbot => val wildcard = orgSet.contains("*") || orgSet.contains("") - if (ident.isMultiTenantAgbot && !wildcard) { // its an IBM Agbot, get all changes from orgs the agbot covers - qFilter = qFilter.filter(_.orgId inSet orgSet) - // if the caller agbot sends in the wildcard case then we don't want to filter on orgId at all, so don't add any more filters. that's why there's just no code written for that case - } else if (!ident.isMultiTenantAgbot) qFilter = qFilter.filter(u => (u.orgId === orgId) || (u.orgId =!= orgId && u.public === "true")) // if its not an IBM agbot use the general case + if (ident.isMultiTenantAgbot && !wildcard) { // its an IBM Agbot with no wildcard sent in, get all changes from orgs the agbot covers + qFilter = qFilter.filter(_.orgId inSet orgSet).filterNot(_.resource === "nodemsgs").filterNot(_.resource === "nodestatus").filterNot(u => u.resource === "nodeagreements" && u.operation === ResourceChangeConfig.CREATEDMODIFIED).filterNot(u => u.resource === "agbotagreements" && u.operation === ResourceChangeConfig.CREATEDMODIFIED).filterNot(u => u.resource === "agbotmsgs" && u.operation === ResourceChangeConfig.DELETED) + } else if ( ident.isMultiTenantAgbot && wildcard) { + // if the IBM agbot sends in the wildcard case then we don't want to filter on orgId at all + qFilter = qFilter.filterNot(_.resource === "nodemsgs").filterNot(_.resource === "nodestatus").filterNot(u => u.resource === "nodeagreements" && u.operation === ResourceChangeConfig.CREATEDMODIFIED).filterNot(u => u.resource === "agbotagreements" && u.operation === ResourceChangeConfig.CREATEDMODIFIED).filterNot(u => u.resource === "agbotmsgs" && u.operation === ResourceChangeConfig.DELETED) + } else { + qFilter = qFilter.filter(u => (u.orgId === orgId) || (u.orgId =!= orgId && u.public === "true")).filterNot(_.resource === "nodemsgs").filterNot(_.resource === "nodestatus").filterNot(u => u.resource === "nodeagreements" && u.operation === ResourceChangeConfig.CREATEDMODIFIED).filterNot(u => u.resource === "agbotagreements" && u.operation === ResourceChangeConfig.CREATEDMODIFIED).filterNot(u => u.resource === "agbotmsgs" && u.operation === ResourceChangeConfig.DELETED) // if its not an IBM agbot only allow access to the agbot's own org and public changes from other orgs + } case _ => qFilter = qFilter.filter(u => (u.orgId === orgId) || (u.orgId =!= orgId && u.public === "true")) } // sort by changeId and take only maxRecords from the query diff --git a/src/test/scala/exchangeapi/AgbotsSuite.scala b/src/test/scala/exchangeapi/AgbotsSuite.scala index 19db0182..c6d87a53 100644 --- a/src/test/scala/exchangeapi/AgbotsSuite.scala +++ b/src/test/scala/exchangeapi/AgbotsSuite.scala @@ -771,7 +771,7 @@ class AgbotsSuite extends AnyFunSuite { test("POST /orgs/"+orgid+"/changes - verify " + agbotId + " agreement was added and stored") { val time = ApiTime.pastUTC(secondsAgo) val input = ResourceChangesRequest(0L, Some(time), maxRecords, None) - val response = Http(URL+"/changes").postData(write(input)).method("post").headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString + val response = Http(URL+"/changes").postData(write(input)).method("post").headers(CONTENT).headers(ACCEPT).headers(USERAUTH).asString info("code: "+response.code) assert(response.code === HttpCode.POST_OK.intValue) assert(!response.body.isEmpty) @@ -779,6 +779,18 @@ class AgbotsSuite extends AnyFunSuite { assert(parsedBody.changes.exists(y => {(y.id == agbotId) && (y.operation == ResourceChangeConfig.CREATEDMODIFIED) && (y.resource == "agbotagreements")})) } + test("POST /orgs/"+orgid+"/changes - verify " + agbotId + " agreement creation not seen by agbots") { + val time = ApiTime.pastUTC(secondsAgo) + val input = ResourceChangesRequest(0L, Some(time), maxRecords, None) + val response = Http(URL+"/changes").postData(write(input)).method("post").headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString + info("code: "+response.code) + assert(response.code === HttpCode.POST_OK.intValue) + assert(!response.body.isEmpty) + val parsedBody = parse(response.body).extract[ResourceChangesRespObject] + assert(!parsedBody.changes.exists(y => {(y.id == agbotId) && (y.operation == ResourceChangeConfig.CREATEDMODIFIED) && (y.resource == "agbotagreements")})) + assert(!parsedBody.changes.exists(y => {(y.operation == ResourceChangeConfig.CREATEDMODIFIED) && (y.resource == "agbotagreements")})) + } + /** Update an agreement for agbot 9930 - as the agbot */ test("PUT /orgs/"+orgid+"/agbots/"+agbotId+"/agreements/"+agreementId+" - update as agbot") { val input = PutAgbotAgreementRequest(AAService(orgid, pattern, "sdr"), "finalized") diff --git a/src/test/scala/exchangeapi/NodesSuite.scala b/src/test/scala/exchangeapi/NodesSuite.scala index ef4330da..70a93cb6 100644 --- a/src/test/scala/exchangeapi/NodesSuite.scala +++ b/src/test/scala/exchangeapi/NodesSuite.scala @@ -1246,6 +1246,18 @@ class NodesSuite extends AnyFunSuite { assert(parsedBody.changes.exists(y => {(y.id == nodeId) && (y.operation == ResourceChangeConfig.CREATEDMODIFIED) && (y.resource == "nodestatus")})) } + test("POST /orgs/"+orgid+"/changes - verify " + agbotId + " can't see nodestatus changes") { + val time = ApiTime.pastUTC(secondsAgo) + val input = ResourceChangesRequest(0L, Some(time), maxRecords, None) + val response = Http(URL+"/changes").postData(write(input)).method("post").headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString + info("code: "+response.code) + assert(response.code === HttpCode.POST_OK.intValue) + assert(!response.body.isEmpty) + val parsedBody = parse(response.body).extract[ResourceChangesRespObject] + assert(!parsedBody.changes.exists(y => {(y.id == nodeId) && (y.operation == ResourceChangeConfig.CREATEDMODIFIED) && (y.resource == "nodestatus")})) + assert(!parsedBody.changes.exists(y => {y.resource == "nodestatus"})) + } + test("GET /orgs/"+orgid+"/nodes/"+nodeId+"/status - as node") { val response = Http(URL+"/nodes/"+nodeId+"/status").method("get").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString info("code: "+response.code+", response.body: "+response.body) @@ -1659,6 +1671,18 @@ class NodesSuite extends AnyFunSuite { assert(parsedBody.changes.exists(y => {(y.id == nodeId) && (y.operation == ResourceChangeConfig.CREATEDMODIFIED) && (y.resource == "nodeagreements")})) } + test("POST /orgs/"+orgid+"/changes - verify " + nodeId + " agreement creation not seen by agbot") { + val time = ApiTime.pastUTC(secondsAgo) + val input = ResourceChangesRequest(0L, Some(time), maxRecords, None) + val response = Http(URL+"/changes").postData(write(input)).method("post").headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString + info("code: "+response.code) + assert(response.code === HttpCode.POST_OK.intValue) + assert(!response.body.isEmpty) + val parsedBody = parse(response.body).extract[ResourceChangesRespObject] + assert(!parsedBody.changes.exists(y => {(y.id == nodeId) && (y.operation == ResourceChangeConfig.CREATEDMODIFIED) && (y.resource == "nodeagreements")})) + assert(!parsedBody.changes.exists(y => {(y.operation == ResourceChangeConfig.CREATEDMODIFIED) && (y.resource == "nodeagreements")})) + } + test("PUT /orgs/"+orgid+"/nodes/"+nodeId+"/agreements/"+agreementId+" - update sdr agreement as node") { val input = PutNodeAgreementRequest(Some(List(NAService(orgid,SDRSPEC_URL))), Some(NAgrService(orgid,patid,SDRSPEC)), "finalized") val response = Http(URL+"/nodes/"+nodeId+"/agreements/"+agreementId).postData(write(input)).method("put").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString @@ -2299,6 +2323,18 @@ class NodesSuite extends AnyFunSuite { assert(parsedBody.changes.exists(y => {(y.id == nodeId) && (y.operation == ResourceChangeConfig.CREATED) && (y.resource == "nodemsgs")})) } + test("POST /orgs/"+orgid+"/changes - verify " + agbotId + " can't see nodemsgs") { + val time = ApiTime.pastUTC(secondsAgo) + val input = ResourceChangesRequest(0L, Some(time), maxRecords, None) + val response = Http(URL+"/changes").postData(write(input)).method("post").headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString + info("code: "+response.code) + assert(response.code === HttpCode.POST_OK.intValue) + assert(!response.body.isEmpty) + val parsedBody = parse(response.body).extract[ResourceChangesRespObject] + assert(!parsedBody.changes.exists(y => {(y.id == nodeId) && (y.operation == ResourceChangeConfig.CREATED) && (y.resource == "nodemsgs")})) + assert(!parsedBody.changes.exists(y => {y.resource == "nodemsgs"})) + } + test("POST /orgs/"+orgid+"/nodes/"+nodeId+"/msgs - short ttl so it will expire") { val input = PostNodesMsgsRequest("{msg1 from agbot1 to node1 with 1 second ttl}", 1) val response = Http(URL+"/nodes/"+nodeId+"/msgs").postData(write(input)).method("post").headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString @@ -2495,15 +2531,26 @@ class NodesSuite extends AnyFunSuite { assert(response.code === HttpCode.DELETED.intValue) info("POST /orgs/"+orgid+"/changes - verify " + agbotId2 + " msg deleted and stored") - val time = ApiTime.pastUTC(secondsAgo) - val resInput = ResourceChangesRequest(0L, Some(time), maxRecords, None) + var time = ApiTime.pastUTC(secondsAgo) + var resInput = ResourceChangesRequest(0L, Some(time), maxRecords, None) response = Http(URL+"/changes").postData(write(resInput)).method("post").headers(CONTENT).headers(ACCEPT).headers(USERAUTH).asString info("code: "+response.code) assert(response.code === HttpCode.POST_OK.intValue) assert(!response.body.isEmpty) - val parsedBody = parse(response.body).extract[ResourceChangesRespObject] + var parsedBody = parse(response.body).extract[ResourceChangesRespObject] assert(parsedBody.changes.exists(y => {(y.id == agbotId2) && (y.operation == ResourceChangeConfig.DELETED) && (y.resource == "agbotmsgs")})) + info("POST /orgs/"+orgid+"/changes - verify " + agbotId2 + " msg deletion not seen by agbots in changes table") + time = ApiTime.pastUTC(secondsAgo) + resInput = ResourceChangesRequest(0L, Some(time), maxRecords, None) + response = Http(URL+"/changes").postData(write(resInput)).method("post").headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString + info("code: "+response.code) + assert(response.code === HttpCode.POST_OK.intValue) + assert(!response.body.isEmpty) + parsedBody = parse(response.body).extract[ResourceChangesRespObject] + assert(!parsedBody.changes.exists(y => {(y.id == agbotId2) && (y.operation == ResourceChangeConfig.DELETED) && (y.resource == "agbotmsgs")})) + assert(!parsedBody.changes.exists(y => {(y.operation == ResourceChangeConfig.DELETED) && (y.resource == "agbotmsgs")})) + response = Http(URL+"/agbots/"+agbotId2+"/msgs").method("get").headers(ACCEPT).headers(AGBOT2AUTH).asString info("code: "+response.code+", response.body: "+response.body) assert(response.code === HttpCode.NOT_FOUND.intValue)