Skip to content

Commit

Permalink
#6 - Added validation. #8 clarified error message
Browse files Browse the repository at this point in the history
  • Loading branch information
hohonuuli committed Jan 27, 2025
1 parent f75e625 commit 3a24411
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ trait AuthorizationEndpointsSuite extends DatabaseFunSuite with EndpointsSuite:
val bearerAuth = d.getOrElse(throw new Exception("No bearer auth"))
assert(jwtService.verify(bearerAuth.access_token))

test("login"):
test("login (ADMINISTRATOR)"):
val userService = UserAccountService(entityManagerFactory)
val userAccount = UserAccount(
"test1234",
Expand Down Expand Up @@ -89,3 +89,29 @@ trait AuthorizationEndpointsSuite extends DatabaseFunSuite with EndpointsSuite:
assert(d.isRight)
val bearerAuth = d.getOrElse(throw new Exception("No bearer auth"))
assert(jwtService.verify(bearerAuth.access_token))

test("login (READONLY)"):
val userService = UserAccountService(entityManagerFactory)
val userAccount = UserAccount(
"test12345",
"SuperSecretPassword",
UserAccountRoles.READONLY.getRoleName,
isEncrypted = Some(false)
)
userService.create(userAccount) match
case Left(e) => fail(e.getMessage)
case Right(ua) =>
val backendStub = newBackendStub(authorizationEndpoints.loginEndpointImpl)

val credentials = Base64.getEncoder.encodeToString(s"${ua.username}:${userAccount.password}".getBytes)
val response = basicRequest
.post(uri"http://test.com/v1/auth/login")
.header("Authorization", s"BASIC $credentials")
.send(backendStub)
.join

response.body match
case Left(e) =>
// this is expected. READONLY users cannot login
case Right(body) =>
fail("READONLY user should not be able to login")
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import org.mbari.oni.services.UserAuthMixin
import sttp.model.StatusCode

import scala.concurrent.ExecutionContext
import org.mbari.oni.jpa.entities.TestEntityFactory
import org.mbari.oni.jpa.entities.TestEntityFactory.randomRankLevelAndName

trait ConceptEndpointsSuite extends EndpointsSuite with DataInitializer with UserAuthMixin:

Expand Down Expand Up @@ -166,11 +168,14 @@ trait ConceptEndpointsSuite extends EndpointsSuite with DataInitializer with Use
user =>
val root = init(2, 0)
val name = root.getPrimaryConceptName.getName
val (rankLevel, rankName) = TestEntityFactory.randomRankLevelAndName()
val expectedRank = Some(s"${{rankLevel.getOrElse("")}}${{rankName.getOrElse("")}}")

val conceptCreate = ConceptCreate(
"SomeChildConcept",
Some(root.getPrimaryConceptName.getName),
rankLevel = Some("yoyoyo"),
rankName = Some("yayaya"),
rankLevel = rankLevel,
rankName = rankName,
aphiaId = Some(54321L)
)

Expand All @@ -182,7 +187,7 @@ trait ConceptEndpointsSuite extends EndpointsSuite with DataInitializer with Use
assertEquals(response.code, StatusCode.Ok)
val concept = checkResponse[ConceptMetadata](response.body)
assertEquals(concept.name, "SomeChildConcept")
assertEquals(concept.rank, Some("yoyoyoyayaya"))
assertEquals(concept.rank, expectedRank)
,
jwt = jwtService.login(user.username, password, user.toEntity)
)
Expand All @@ -202,10 +207,12 @@ trait ConceptEndpointsSuite extends EndpointsSuite with DataInitializer with Use
user =>
val root = init(3, 0)
val grandChild = root.getChildConcepts.iterator().next().getChildConcepts.iterator().next()
val (rankLevel, rankName) = TestEntityFactory.randomRankLevelAndName()
val expectedRank = Some(s"${{rankLevel.getOrElse("")}}${{rankName.getOrElse("")}}")
val conceptUpdate = ConceptUpdate(
Some(root.getPrimaryConceptName.getName),
rankLevel = Some("yoyoyo"),
rankName = Some("yayaya"),
rankLevel = rankLevel,
rankName = rankName,
aphiaId = Some(543210L)
)

Expand All @@ -217,7 +224,7 @@ trait ConceptEndpointsSuite extends EndpointsSuite with DataInitializer with Use
assertEquals(response.code, StatusCode.Ok)
val concept = checkResponse[ConceptMetadata](response.body)
assertEquals(concept.name, grandChild.getPrimaryConceptName.getName)
assertEquals(concept.rank, Some("yoyoyoyayaya"))
assertEquals(concept.rank, expectedRank)
,
jwt = jwtService.login(user.username, password, user.toEntity)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,25 @@ trait UserAccountsEndpointsSuite extends EndpointsSuite with DataInitializer:
)
}

test("createEndpoint (ReadOnly from JSON)") {
val entity = TestEntityFactory.createUserAccount(UserAccountRoles.READONLY.getRoleName)
val userAccount = UserAccount.from(entity).copy(password = Strings.random(10))


runPost(
endpoints.createEndpointImpl,
"http://test.com/v1/users",
userAccount.stringify,
response =>
assertEquals(response.code, StatusCode.Ok)
val obtained = checkResponse[UserAccount](response.body)
assertEquals(obtained.copy(id = None, password = userAccount.password), userAccount)
,
jwt = jwtService.authorize(jwtService.apiKey)
)

}

test("createEndpoint (form camelCase)") {
val entity = TestEntityFactory.createUserAccount(UserAccountRoles.ADMINISTRATOR.getRoleName)
val userAccount = UserAccount.from(entity).copy(password = Strings.random(10))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import java.net.URI
import java.time.Instant
import java.util.Date
import java.util.concurrent.atomic.AtomicLong
import org.mbari.oni.services.RankValidator

object TestEntityFactory:

Expand Down Expand Up @@ -52,7 +53,8 @@ object TestEntityFactory:
for _ <- 0 until numberChildren do
val entity = buildNode(maxBreadth)
if entity.getRankLevel != null then // Add a numbert depth to the rank level
entity.setRankLevel(s"$depth--${entity.getRankLevel}")
// entity.setRankLevel(s"$depth--${entity.getRankLevel}")
entity.setRankLevel(entity.getRankLevel)
// println(s"------- Adding " + entity.getPrimaryConceptName.getName + " to " + parent.getPrimaryConceptName.getName)
parent.addChildConcept(entity)
buildTree(entity, depth - 1, maxBreadth)
Expand Down Expand Up @@ -155,8 +157,9 @@ object TestEntityFactory:
entity.addConceptName(createConceptName())
entity.setConceptMetadata(createConceptMetadata())
if random.nextBoolean() then
entity.setRankLevel(Strings.random(6))
entity.setRankName(Strings.random(12))
val (rankLevel, rankName) = randomRankLevelAndName()
entity.setRankLevel(rankLevel.orNull)
entity.setRankName(rankName.orNull)

if random.nextBoolean() then entity.setAphiaId(random.nextLong(100000000))

Expand Down Expand Up @@ -192,3 +195,8 @@ object TestEntityFactory:
// entity.setCitation(s"B. M. Schlining. 1968. $s")
entity.setCitation(s)
entity


def randomRankLevelAndName(): (Option[String], Option[String]) =
val idx = random.nextInt(RankValidator.ValidRanks.size)
RankValidator.ValidRankLevelsAndNames(idx)
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,8 @@ trait ConceptServiceSuite extends DatabaseFunSuite with UserAuthMixin:
updatedChild <- conceptService.update(
child.getPrimaryConceptName.getName,
ConceptUpdate(
rankLevel = Some("supersuper"),
rankName = Some("genera"),
rankLevel = Some("sub"),
rankName = Some("genus"),
aphiaId = Some(1234)
),
user.username
Expand All @@ -280,7 +280,7 @@ trait ConceptServiceSuite extends DatabaseFunSuite with UserAuthMixin:
case Left(e) =>
fail("Failed to update")
case Right(child) =>
assertEquals(child.rank, Some("supersupergenera"))
assertEquals(child.rank, Some("subgenus"))
assertEquals(child.aphiaId, Some(1234L))

historyService.findByConceptName(child.name) match
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class AuthorizationEndpoints(entityManagerFactory: EntityManagerFactory)(using j
entity <- Right(userAccount.toEntity)
jwt <- jwtService
.login(usernamePassword.username, usernamePassword.password.getOrElse(""), entity)
.toRight(Unauthorized("Invalid username or password"))
.toRight(Unauthorized("Unable to login. Check your username and password and verify that you are an administrator or maintainer"))
yield AuthorizationSC.bearer(jwt)
}
.serverLogic(bearerAuth => Unit => Future(Right(bearerAuth)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ class ConceptService(entityManagerFactory: EntityManagerFactory):
repo.findByName(conceptCreate.name).toScala match
case Some(_) => throw ConceptNameAlreadyExists(conceptCreate.name)
case None =>
RankValidator.throwExceptionIfInvalid(conceptCreate)
val parent = conceptCreate.parentName match
case Some(parentName) =>
repo.findByName(parentName).toScala match
Expand Down Expand Up @@ -365,6 +366,7 @@ class ConceptService(entityManagerFactory: EntityManagerFactory):
repo.findByName(name).toScala match
case None => throw ConceptNameNotFound(name)
case Some(conceptEntity) =>
RankValidator.throwExceptionIfInvalid(conceptUpdate)
updateParent(userEntity, conceptEntity, conceptUpdate.parentName)
updateRankLevel(userEntity, conceptEntity, conceptUpdate.rankLevel)
updateRankName(userEntity, conceptEntity, conceptUpdate.rankName)
Expand Down
89 changes: 89 additions & 0 deletions oni/src/main/scala/org/mbari/oni/services/RankValidator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) Monterey Bay Aquarium Research Institute 2024
*
* oni code is non-public software. Unauthorized copying of this file,
* via any medium is strictly prohibited. Proprietary and confidential.
*/

package org.mbari.oni.services

import org.mbari.oni.domain.ConceptCreate
import org.mbari.oni.domain.ConceptUpdate

object RankValidator:

val ValidRankLevelsAndNames: Seq[(Option[String], Option[String])] = Seq(
(None, None),
(None, Some("realm")),
(Some("sub"), Some("realm")),
(None, Some("kingdom")),
(Some("sub"), Some("kingdom")),
(None, Some("phylum")),
(Some("sub"), Some("phylum")),
(None, Some("division")),
(Some("sub"), Some("division")),
(None, Some("class")),
(Some("sub"), Some("class")),
(Some("super"), Some("order")),
(None, Some("order")),
(Some("sub"), Some("order")),
(Some("infra"), Some("order")),
(Some("super"), Some("family")),
(Some("epi"), Some("family")),
(None, Some("family")),
(Some("sub"), Some("family")),
(Some("infra"), Some("family")),
(None, Some("tribe")),
(Some("sub"), Some("tribe")),
(Some("infra"), Some("tribe")),
(None, Some("genus")),
(Some("sub"), Some("genus")),
(None, Some("section")),
(Some("sub"), Some("section")),
(None, Some("species complex")),
(None, Some("species")),
(Some("sub"), Some("species")),
(None, Some("variety")),
(None, Some("form"))
)

/**
* A list of commonlhy accepted, valid ranks scraped from Wikipedia
*/
val ValidRanks: Seq[String] = ValidRankLevelsAndNames.map {
(rankLevel, rankName) => s"${rankLevel.getOrElse("")}${rankName.getOrElse("")}".toLowerCase
}

def validate(rankLevel: Option[String] = None, rankName: Option[String] = None): Boolean = {
val rank = s"${rankLevel.getOrElse("")}${rankName.getOrElse("")}".toLowerCase
ValidRanks.contains(rank)
}

def validate(conceptCreate: ConceptCreate): Boolean = {
validate(conceptCreate.rankLevel, conceptCreate.rankName)
}

def validate(conceptUpdate: ConceptUpdate): Boolean = {
validate(conceptUpdate.rankLevel, conceptUpdate.rankName)
}

def throwExceptionIfInvalid(conceptCreate: ConceptCreate): Unit = {
if (!validate(conceptCreate)) {
val rank = s"${conceptCreate.rankLevel.getOrElse("")}${conceptCreate.rankName.getOrElse("")}"
throw new IllegalArgumentException(
s"Invalid rank level + rank name ($rank). Should be one of ${RankValidator.ValidRanks.mkString(", ")}"
)
}
}

def throwExceptionIfInvalid(conceptUpdate: ConceptUpdate): Unit = {
if (!validate(conceptUpdate.rankLevel, conceptUpdate.rankName)) {
val rank = s"${conceptUpdate.rankLevel.getOrElse("")}${conceptUpdate.rankName.getOrElse("")}"
throw new IllegalArgumentException(
s"Invalid rank level + rank name ($rank). Should be one of ${RankValidator.ValidRanks.mkString(", ")}"
)
}

}


2 changes: 1 addition & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ object Dependencies {
lazy val circeGeneric = "io.circe" %% "circe-generic" % circeVersion
lazy val circeParser = "io.circe" %% "circe-parser" % circeVersion

lazy val commonsCodec = "commons-codec" % "commons-codec" % "1.17.2"
lazy val commonsCodec = "commons-codec" % "commons-codec" % "1.18.0"
lazy val gson = "com.google.code.gson" % "gson" % "2.11.0"

// THis needs to match the version used by tapirHelidon.
Expand Down

0 comments on commit 3a24411

Please sign in to comment.