Skip to content

Commit

Permalink
Merge pull request #684 from naphelps/issue-675
Browse files Browse the repository at this point in the history
Issue 675: Node tokens can be changed by all User types, and public keys can only be [un]set without changing keys by Nodes.
  • Loading branch information
naphelps authored Jul 7, 2023
2 parents c0fb74c + 5641002 commit 5e25a7c
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 63 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

All notable changes to this project will be documented in this file.

## [2.115.3] - 2023-07-07
- Issue 675: Node tokens can be changed by all User types, and public keys can only be [un]set without changing keys by Nodes.

## [2.115.2] - 2023-07-07
- Issue 675: Removed extra regular expressions modifying searched Service's URL.

Expand Down
2 changes: 1 addition & 1 deletion docs/openapi-3-developer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name" : "Apache License Version 2.0",
"url" : "https://www.apache.org/licenses/LICENSE-2.0"
},
"version" : "2.115.2"
"version" : "2.115.3"
},
"externalDocs" : {
"description" : "Open-horizon ExchangeAPI",
Expand Down
2 changes: 1 addition & 1 deletion docs/openapi-3-user.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name" : "Apache License Version 2.0",
"url" : "https://www.apache.org/licenses/LICENSE-2.0"
},
"version" : "2.115.2"
"version" : "2.115.3"
},
"externalDocs" : {
"description" : "Open-horizon ExchangeAPI",
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/messages.txt
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ policy.added.or.updated=policy added or updated
policy.not.inserted.or.updated=policy for service ''{0}'' not inserted or updated: {1}
post.ok=post ok
property.type.must.be=The properties.type value ''{0}'' must be 1 of: {1}
public.key.no.replace=A node's public key cannot be replaced with a different key.
reload.successful=reload successful
req.service.has.wrong.arch=required service ''{0}'' has arch ''{1}'', which is different than this service''s arch ''{2}''
req.service.not.in.exchange=the following required service does not exist in the exchange: org= {0}, url= {1}, version= {2}, arch= {3}
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.115.2
2.115.3
59 changes: 41 additions & 18 deletions src/main/scala/org/openhorizon/exchangeapi/route/node/Node.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import slick.dbio.DBIO
import slick.jdbc.PostgresProfile.api._
import slick.lifted.{Compiled, CompiledExecutable}

import java.lang.IllegalStateException
import java.sql.Timestamp
import java.time.ZoneId
import scala.concurrent.ExecutionContext
Expand Down Expand Up @@ -331,7 +332,7 @@ trait Node extends JacksonSupport with AuthenticationSupport {
val session: String = Password.fastHash(password = s"patch$organization$node${identity.identityString}${changeTimestamp.getTime.toString}")

// Have a single attribute to update, retrieve its name.
val vaildAttribute: String =
val validAttribute: String =
attributeExistence.filter(attribute => attribute._2).head._1

val getPatternBase: Query[Patterns, PatternRow, Seq] =
Expand All @@ -351,7 +352,7 @@ trait Node extends JacksonSupport with AuthenticationSupport {
.length)

val servicesToSearch: Seq[SearchServiceKey] = {
if (vaildAttribute == "userInput") {
if (validAttribute == "userInput") {
reqBody.userInput.get.map(
service =>
SearchServiceKey(architecture = (if (service.serviceArch.isEmpty || service.serviceArch.get == "") "%" else service.serviceArch.get),
Expand All @@ -377,10 +378,11 @@ trait Node extends JacksonSupport with AuthenticationSupport {
service.url,
service.version))

val patchNode: DBIOAction[Unit, NoStream, Effect.Read with Effect with Effect.Write] =
val patchNode =
for {
// ---------- pattern --------------
matchedPatterns <-
if (vaildAttribute == "pattern" && reqBody.pattern.get.nonEmpty)
if (validAttribute == "pattern" && reqBody.pattern.get.nonEmpty)
matchingPatterns.result
else
DBIO.successful(1)
Expand All @@ -389,16 +391,37 @@ trait Node extends JacksonSupport with AuthenticationSupport {
// already using a Deployment Policy.
_ <-
if (matchedPatterns == 0)
DBIO.failed(new BadInputException(ExchMsg.translate("pattern.not.in.exchange")))
DBIO.failed(new BadInputException(msg = ExchMsg.translate("pattern.not.in.exchange")))
else
DBIO.successful(())

// ---------- publicKey ------------
validPublicKey <-
if ((identity.role.equals(AuthRoles.Node) ||
identity.isSuperUser) &&
validAttribute == "publicKey") {
if (reqBody.publicKey.get.nonEmpty)
Compiled(NodesTQ.getNode(resource)
.filter(_.publicKey === "")
.map(_.id)
.length).result
else
DBIO.successful(1)
} else if (validAttribute == "publicKey")
DBIO.failed(new AccessDeniedException(msg = ExchMsg.translate("access.denied")))
else
DBIO.successful(1)

_ <-
if (validPublicKey == 0)
DBIO.failed(new BadInputException(msg = ExchMsg.translate("public.key.no.replace")))
else
DBIO.successful(())

// ---------- token ----------------
unsetPublicKeys <-
if ((identity.role.equals(AuthRoles.Node) || identity.isSuperUser) &&
vaildAttribute == "token")
if (validAttribute == "token")
Compiled(NodesTQ.getNode(resource).filter(_.publicKey === "").map(_.id).length).result
else if (vaildAttribute == "token")
DBIO.failed(new AccessDeniedException(msg = ExchMsg.translate("access.denied")))
else
DBIO.successful(1)

Expand All @@ -410,22 +433,22 @@ trait Node extends JacksonSupport with AuthenticationSupport {
DBIO.successful(())

hashedPW =
if (vaildAttribute == "token")
if (validAttribute == "token")
Password.fastHash(reqBody.token.get)
else
""

// ---------- userInput ------------
// Has to have valid matches with defined services. Must be authorized to use service.
_ <-
if (vaildAttribute == "userInput") {
if (validAttribute == "userInput") {
// Load services to search on
SearchServiceTQ ++= servicesToSearch
} else
DBIO.successful(())

unmatchedServices <-
if (vaildAttribute == "userInput") {
if (validAttribute == "userInput") {
// Compare our request input with what we have, factoring in authorization.
Compiled(authorizedServices.joinRight(
SearchServiceTQ.filter(_.session === session)
Expand Down Expand Up @@ -453,7 +476,7 @@ trait Node extends JacksonSupport with AuthenticationSupport {
DBIO.successful(Seq(("", "", "", ""))) // Needs to be of the same type.

_ <-
if (vaildAttribute == "userInput") {
if (validAttribute == "userInput") {
// Either all of our requested services are valid or none of them are.
if (0 < unmatchedServices.length) {
// Database will auto-remove our inputs when the transaction rolls-back.
Expand All @@ -468,7 +491,7 @@ trait Node extends JacksonSupport with AuthenticationSupport {

// ---------- Node Record Change ---
nodesUpdated <-
if (vaildAttribute == "clusterNamespace")
if (validAttribute == "clusterNamespace")
Compiled(NodesTQ.getNode(resource)
.map(node => (node.clusterNamespace, node.lastUpdated)))
.update((reqBody.clusterNamespace,
Expand All @@ -479,7 +502,7 @@ trait Node extends JacksonSupport with AuthenticationSupport {
else
Compiled(NodesTQ.getNode(resource)
.map(node =>
(vaildAttribute match {
(validAttribute match {
case "arch" => node.arch
case "heartbeatIntervals" => node.heartbeatIntervals
case "msgEndPoint" => node.msgEndPoint
Expand All @@ -492,7 +515,7 @@ trait Node extends JacksonSupport with AuthenticationSupport {
case "token" => node.token
case "userInput" => node.userInput},
node.lastUpdated)))
.update((vaildAttribute match {
.update((validAttribute match {
case "arch" => reqBody.arch.get
case "heartbeatIntervals" => write(reqBody.heartbeatIntervals.get)
case "msgEndPoint" => reqBody.msgEndPoint.get
Expand Down Expand Up @@ -530,7 +553,7 @@ trait Node extends JacksonSupport with AuthenticationSupport {

// ---------- Update Auth Cache ----
_ <-
if (vaildAttribute == "token") {
if (validAttribute == "token") {
AuthCache.putNode(resource, hashedPW, reqBody.token.get)
DBIO.successful(())
}
Expand All @@ -541,7 +564,7 @@ trait Node extends JacksonSupport with AuthenticationSupport {
db.run(patchNode.transactionally.asTry)
.map({
case Success(_) =>
(HttpCode.PUT_OK, ApiResponse(ApiRespType.OK, ExchMsg.translate("node.attribute.updated", vaildAttribute, resource)))
(HttpCode.PUT_OK, ApiResponse(ApiRespType.OK, ExchMsg.translate("node.attribute.updated", validAttribute, resource)))
case Failure(t: org.postgresql.util.PSQLException) =>
ExchangePosgtresErrorHandling.ioProblemError(t, ExchMsg.translate("node.not.inserted.or.updated", resource, t.getMessage))
case Failure(t: AccessDeniedException) =>
Expand Down
41 changes: 29 additions & 12 deletions src/test/scala/org/openhorizon/exchangeapi/NodesSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,22 @@ class NodesSuite extends AnyFunSuite with BeforeAndAfterAll {
var jsonInput = """{ "publicKey": """"+nodePubKey+"""" }"""
var response = Http(URL+"/nodes/"+nodeId).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.BAD_INPUT.intValue)

jsonInput = """{ "publicKey": "" }"""
response = Http(URL+"/nodes/"+nodeId).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
info("code: " + response.code + ", response.body: " + response.body)
assert(response.code === HttpCode.PUT_OK.intValue)

// whitespace
jsonInput = """ { "publicKey": "" } """
response = Http(URL + "/nodes/" + nodeId).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
info("code: " + response.code + ", response.body: " + response.body)
assert(response.code === HttpCode.PUT_OK.intValue)

jsonInput = """{ "publicKey": """" + nodePubKey + """" }"""
response = Http(URL + "/nodes/" + nodeId).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
info("code: " + response.code + ", response.body: " + response.body)
assert(response.code === HttpCode.PUT_OK.intValue)

jsonInput = """{ "userInput": [{ "serviceOrgid": """"+orgid+"""", "serviceUrl": """"+SDRSPEC_URL+"""", "serviceArch": """"+svcarch+"""", "serviceVersionRange": """"+ALL_VERSIONS+"""", "inputs": [{"name":"UI_STRING","value":"mystr - updated"}, {"name":"UI_INT","value": 7}, {"name":"UI_BOOLEAN","value": true}] }] }"""
Expand All @@ -1262,18 +1278,13 @@ class NodesSuite extends AnyFunSuite with BeforeAndAfterAll {
}

test("PATCH /orgs/"+orgid+"/nodes/"+nodeId+" - as node with whitespace") {
var jsonInput = """ { "publicKey": """"+nodePubKey+"""" } """
var response = Http(URL+"/nodes/"+nodeId).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.PUT_OK.intValue)

jsonInput =
val jsonInput =
"""
{ "userInput": [{ "serviceOrgid": """"+orgid+"""", "serviceUrl": """"+SDRSPEC_URL+"""", "serviceArch": """"+svcarch+"""", "serviceVersionRange": """"+ALL_VERSIONS+
"""", "inputs": [{"name":"UI_STRING","value":"mystr - updated"}, {"name":"UI_INT","value": 7}, {"name":"UI_BOOLEAN","value": true}] }] }
"""
response = Http(URL+"/nodes/"+nodeId).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
val response = Http(URL+"/nodes/"+nodeId).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.PUT_OK.intValue)
}
Expand Down Expand Up @@ -1328,8 +1339,11 @@ class NodesSuite extends AnyFunSuite with BeforeAndAfterAll {
var prevLastUpdated = dev.lastUpdated

// patch the node so that the lastUpdated field gets updated
val jsonInput = """{ "publicKey": """"+nodePubKey+"""" }"""
response = Http(URL+"/nodes/"+nodeId4).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(USERAUTH).asString
var jsonInput = """{ "publicKey": "" }"""
response = Http(URL + "/nodes/" + nodeId4).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(ROOTAUTH).asString

jsonInput = """{ "publicKey": """"+nodePubKey+"""" }"""
response = Http(URL+"/nodes/"+nodeId4).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(ROOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.PUT_OK.intValue)

Expand Down Expand Up @@ -1437,7 +1451,7 @@ class NodesSuite extends AnyFunSuite with BeforeAndAfterAll {

test("PATCH /orgs/"+orgid+"/nodes/"+nodeId3+" - add publicKey so it will be found") {
val jsonInput = """{ "publicKey": "NODE3ABC" }"""
val response = Http(URL + "/nodes/" + nodeId3).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(USERAUTH).asString
val response = Http(URL + "/nodes/" + nodeId3).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(ROOTAUTH).asString
assert(response.code === HttpCode.PUT_OK.intValue)

patchAllNodePatterns("") // remove pattern from nodes so we can search for services
Expand Down Expand Up @@ -2342,8 +2356,11 @@ class NodesSuite extends AnyFunSuite with BeforeAndAfterAll {
}

test("PATCH /orgs/"+orgid+"/nodes/"+nodeId2+" - patching public key so this node won't be stale for non-pattern search") {
val jsonInput = """{ "publicKey": """"+nodePubKey+"""" }"""
val response = Http(URL + "/nodes/" + nodeId2).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(USERAUTH).asString
var jsonInput = """{ "publicKey": "" }"""
var response = Http(URL + "/nodes/" + nodeId2).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(ROOTAUTH).asString

jsonInput = """{ "publicKey": """"+nodePubKey+"""" }"""
response = Http(URL + "/nodes/" + nodeId2).postData(jsonInput).method("PATCH").headers(CONTENT).headers(ACCEPT).headers(ROOTAUTH).asString
info("PATCH "+nodeId2+", code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.PUT_OK.intValue)
}
Expand Down
Loading

0 comments on commit 5e25a7c

Please sign in to comment.