Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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