Skip to content

Commit

Permalink
Merge pull request #635 from ewee33/issue-625
Browse files Browse the repository at this point in the history
Issue 625 - ability to add hub admins on start-up
  • Loading branch information
lorenzoking authored Aug 18, 2022
2 parents ca6aa7e + 82500fd commit aa725df
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 33 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,11 @@ Akka-Http: https://doc.akka.io/docs/akka-http/current/configuration.html
- 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.104.1
## Changes in 2.105.0
- Issue 625: Added ability to create hub admins on start-up in config.json
- Issue 625: Added ability to define an account id for the root org on start-up

## Changes in 2.104.1
- Fixes org.scoverage dependency issues

## Changes in 2.104.0
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.104.1"
"version" : "2.105.0"
},
"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.104.1"
"version" : "2.105.0"
},
"externalDocs" : {
"description" : "Open-horizon ExchangeAPI",
Expand Down
6 changes: 4 additions & 2 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ addSbtPlugin("io.spray" % "sbt-revolver" % "[0.9.1,)")
// addSbtPlugin("org.scalatra.sbt" % "sbt-scalatra" % "latest.release")

// Code coverage report generation
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "[1.6.1,2.0.0]")

addSbtPlugin("org.scoverage" % "sbt-scoverage" % "[1.6.1,)")

ThisBuild / libraryDependencySchemes ++= Seq(
"org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always
)



Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"akka.http.server.idle-timeout": "60s",
"akka.http.server.linger-timeout": "1m",
"akka.http.server.max-connections": "1024",
"akka.http.server.parsing.max-header-name-length": "128",
"akka.http.server.pipelining-limit": "1",
"akka.http.server.request-timeout": "45s",
"akka.http.server.server-header": ""
Expand Down Expand Up @@ -144,6 +145,7 @@
"check_agreement_status": 1800
}
},
"hubadmins": [],
"limits": {
"maxAgbots": 1000,
"maxAgreements": 0,
Expand All @@ -163,6 +165,7 @@
"ttl": 14400
},
"root": {
"account_id": null,
"enabled": true,
"password": ""
},
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.104.1
2.105.0
117 changes: 95 additions & 22 deletions src/main/scala/com/horizon/exchangeapi/ApiUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import scala.jdk.CollectionConverters.{CollectionHasAsScala, MapHasAsJava, MapHa
import scala.util.matching.Regex
import akka.http.scaladsl.model.{StatusCode, StatusCodes}
import akka.http.scaladsl.server._
import com.horizon.exchangeapi.tables.{OrgRow, UserRow}
import com.horizon.exchangeapi.tables.{OrgRow, UserRow, UsersTQ}
import com.osinka.i18n.{Lang, Messages}
import com.typesafe.config._
import slick.jdbc.PostgresProfile.api._
Expand All @@ -25,7 +25,7 @@ import org.json4s.JsonAST.JValue
import org.json4s._
import org.json4s.jackson.Serialization.write

import scala.collection.mutable.{HashMap => MutableHashMap}
import scala.collection.mutable.{ListBuffer, HashMap => MutableHashMap}

/** HTTP codes, taken from https://en.wikipedia.org/wiki/List_of_HTTP_status_codes and https://www.restapitutorial.com/httpstatuscodes.html */
object HttpCode {
Expand Down Expand Up @@ -333,7 +333,8 @@ object ExchConfig {
val rootIsEnabled: Boolean = config.getBoolean("api.root.enabled")
if (rootpw == "" || !rootIsEnabled) {
rootHashedPw = "" // this should already be true, but just make sure
} else { // there is a real, enabled root pw
}
else { // there is a real, enabled root pw
//val hashedPw = Password.hashIfNot(rootpw) <- can't hash this again, because it would be different
if (rootHashedPw == "") logger.error("Internal Error: rootHashedPw not already set")
val rootUnhashedPw: String = if (Password.isHashed(rootpw)) "" else rootpw // this is the 1 case in which an id cache entry could not have an unhashed pw/tok
Expand All @@ -342,25 +343,97 @@ object ExchConfig {
// Put the root org and user in the db, even if root is disabled (because in that case we want all exchange instances to know the root pw is blank
//val rootemail = config.getString("api.root.email")
val rootemail = ""
// Create the root org, create the IBM org, and create the root user (all only if necessary)
db.run(OrgRow("root", "", "Root Org", "Organization for the root user only", ApiTime.nowUTC, None, "", "").upsert.asTry.flatMap({ xs =>
logger.debug("Upsert /orgs/root result: " + xs.toString)
xs match {
case Success(_) => UserRow(Role.superUser, "root", rootHashedPw, admin = true, hubAdmin = true, rootemail, ApiTime.nowUTC, Role.superUser).upsertUser.asTry // next action
case Failure(t) => DBIO.failed(t).asTry // rethrow the error to the next step
}
}).flatMap({ xs =>
logger.debug("Upsert /orgs/root/users/root (root) result: " + xs.toString)
xs match {
case Success(_) => OrgRow("IBM", "IBM", "IBM Org", "Organization containing IBM services", ApiTime.nowUTC, None, "", "").upsert.asTry // next action
case Failure(t) => DBIO.failed(t).asTry // rethrow the error to the next step
}
})).map({ xs =>
logger.debug("Upsert /orgs/IBM result: " + xs.toString)
xs match {
case Success(_) => logger.info("Root org and user from config.json was successfully created/updated in the DB")
case Failure(t) => logger.error("Failed to write the root user from config.json to the DB: " + t.toString)
}
// Create the root org, create the IBM org, create the root user, and create hub admins listed in config.json (all only if necessary)

val rootOrg: OrgRow = OrgRow(
orgId = "root",
orgType = "",
label = "Root Org",
description = "Organization for the root user only",
lastUpdated = ApiTime.nowUTC,
tags = {
try {
Some(JObject("ibmcloud_id" -> JString(config.getString("api.root.account_id"))))
}
catch {
case _: Exception => None
}
},
limits = "",
heartbeatIntervals = ""
)

val rootUser: UserRow = UserRow(
admin = true,
email = rootemail,
hashedPw = rootHashedPw,
hubAdmin = true,
lastUpdated = ApiTime.nowUTC,
orgid = "root",
updatedBy = Role.superUser,
username = Role.superUser
)

val IBMOrg: OrgRow = OrgRow(
description = "Organization containing IBM services",
heartbeatIntervals = "",
label = "IBM Org",
lastUpdated = ApiTime.nowUTC,
limits = "",
orgId = "IBM",
orgType = "IBM",
tags = None
)

val configHubAdmins: ListBuffer[UserRow] = ListBuffer.empty[UserRow]
config.getObjectList("api.hubadmins").asScala.foreach({
c =>
if (c.toConfig.getString("org") == "root") {
configHubAdmins += UserRow(
hashedPw = {
val credential: Option[String] =
try {
Option(c.toConfig.getString("password"))
}
catch {
case _: Exception => None
}
if(credential.isEmpty) "" // No password, IAM User.
else if(Password.isHashed(credential.get)) credential.get // Password is already hashed.
else Password.hash(credential.get) // Plain-text, hash.
},
orgid = c.toConfig.getString("org"),
username = c.toConfig.getString("org") + "/" + c.toConfig.getString("user"),
admin = false,
hubAdmin = true,
email = "",
lastUpdated = ApiTime.nowUTC,
updatedBy = ""
)
}
else {
logger.error(s"Hub Admin '${c.toConfig.getString("user")}' not created: hub admin must be in the root org")
}
})

val query = for {
existingUsers <- UsersTQ.filter(_.username inSet configHubAdmins.map(_.username)).map(_.username).result //get all users whose usernames match a hub admin username in config.json
_ <- DBIO.seq(
rootOrg.upsert,
rootUser.upsertUser,
IBMOrg.upsert,
UsersTQ ++= configHubAdmins.filter(a => !existingUsers.contains(a.username)) //only insert the ones whose usernames don't already exist
)
} yield existingUsers

db.run(query.transactionally.asTry).map({
case Success(result) =>
for (badUser <- result) {
logger.warning(s"Hub Admin '$badUser' not created: a user with this username already exists")
}
logger.info("Successfully updated/inserted root org, root user, IBM org, and hub admins from config")
case Failure(t) =>
logger.error(s"Failed to update/insert root org, root user, IBM org, and hub admins from config: ${t.toString}")
})
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/horizon/exchangeapi/NodeGroupRoutes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ trait NodeGroupRoutes extends JacksonSupport with AuthenticationSupport {
ResChangeCategory.NODE,
false,
ResChangeResource.NODE,
ResChangeOperation.MODIFIED
ResChangeOperation.CREATEDMODIFIED
).toResourceChangeRow)
)
}
Expand Down Expand Up @@ -546,7 +546,7 @@ trait NodeGroupRoutes extends JacksonSupport with AuthenticationSupport {
ResChangeCategory.NODE,
false,
ResChangeResource.NODE,
ResChangeOperation.MODIFIED
ResChangeOperation.CREATEDMODIFIED
).toResourceChangeRow)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ class TestPostNodeGroupRoute extends AnyFunSuite with BeforeAndAfterAll with Bef
assert(dbNodeGroup.description === requestBody.description)
assert(dbNodeGroup.name === "king")
assert(Await.result(DBCONNECTION.getDb.run(ResourceChangesTQ.filter(_.orgId === "TestPostNodeGroupRoute").filter(_.resource === ResChangeResource.NODEGROUP.toString).filter(_.operation === ResChangeOperation.CREATED.toString).result),AWAITDURATION).nonEmpty)
val nodeRCs = Await.result(DBCONNECTION.getDb.run(ResourceChangesTQ.filter(_.orgId === "TestPostNodeGroupRoute").filter(_.resource === ResChangeResource.NODE.toString).filter(_.operation === ResChangeOperation.MODIFIED.toString).result), AWAITDURATION)
val nodeRCs = Await.result(DBCONNECTION.getDb.run(ResourceChangesTQ.filter(_.orgId === "TestPostNodeGroupRoute").filter(_.resource === ResChangeResource.NODE.toString).filter(_.operation === ResChangeOperation.CREATEDMODIFIED.toString).result), AWAITDURATION)
assert(nodeRCs.exists(_.id === "TestPostNodeGroupRoute/node0"))
assert(nodeRCs.exists(_.id === "TestPostNodeGroupRoute/node1"))
assert(nodeRCs.exists(_.id === "TestPostNodeGroupRoute/node2"))
Expand Down Expand Up @@ -294,7 +294,7 @@ class TestPostNodeGroupRoute extends AnyFunSuite with BeforeAndAfterAll with Bef
assert(dbNodeGroup.description === requestBody.description)
assert(dbNodeGroup.name === "queen")
assert(Await.result(DBCONNECTION.getDb.run(ResourceChangesTQ.filter(_.orgId === "TestPostNodeGroupRoute").filter(_.resource === ResChangeResource.NODEGROUP.toString).filter(_.operation === ResChangeOperation.CREATED.toString).result),AWAITDURATION).nonEmpty)
val nodeRCs = Await.result(DBCONNECTION.getDb.run(ResourceChangesTQ.filter(_.orgId === "TestPostNodeGroupRoute").filter(_.resource === ResChangeResource.NODE.toString).filter(_.operation === ResChangeOperation.MODIFIED.toString).result), AWAITDURATION)
val nodeRCs = Await.result(DBCONNECTION.getDb.run(ResourceChangesTQ.filter(_.orgId === "TestPostNodeGroupRoute").filter(_.resource === ResChangeResource.NODE.toString).filter(_.operation === ResChangeOperation.CREATEDMODIFIED.toString).result), AWAITDURATION)
assert(nodeRCs.exists(_.id === "TestPostNodeGroupRoute/node3"))
assert(nodeRCs.exists(_.id === "TestPostNodeGroupRoute/node4"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ class TestPutNodeGroupRoute extends AnyFunSuite with BeforeAndAfterAll with Befo
.filter(_.category === ResChangeCategory.NODE.toString)
.filter(_.public === "false")
.filter(_.resource === ResChangeResource.NODE.toString)
.filter(_.operation === ResChangeOperation.MODIFIED.toString)
.filter(_.operation === ResChangeOperation.CREATEDMODIFIED.toString)
.result), AWAITDURATION).nonEmpty)
}

Expand Down

0 comments on commit aa725df

Please sign in to comment.