diff --git a/it-sqlserver/src/test/scala/org/mbari/oni/endpoints/SqlServerAuthorizationEndpointsSuite.scala b/it-sqlserver/src/test/scala/org/mbari/oni/endpoints/SqlServerAuthorizationEndpointsSuite.scala new file mode 100644 index 0000000..7f588c1 --- /dev/null +++ b/it-sqlserver/src/test/scala/org/mbari/oni/endpoints/SqlServerAuthorizationEndpointsSuite.scala @@ -0,0 +1,7 @@ +package org.mbari.oni.endpoints + +import org.mbari.oni.SqlServerMixin + +class SqlServerAuthorizationEndpointsSuite extends AuthorizationEndpointsSuite with SqlServerMixin { + +} diff --git a/it/src/main/scala/org/mbari/oni/endpoints/AuthorizationEndpointsSuite.scala b/it/src/main/scala/org/mbari/oni/endpoints/AuthorizationEndpointsSuite.scala new file mode 100644 index 0000000..fae35a6 --- /dev/null +++ b/it/src/main/scala/org/mbari/oni/endpoints/AuthorizationEndpointsSuite.scala @@ -0,0 +1,91 @@ +/* + * 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.endpoints + +import io.circe.parser.decode +import org.mbari.oni.domain.{Authorization, UserAccount, UserAccountRoles} +import org.mbari.oni.etc.circe.CirceCodecs.given +import org.mbari.oni.etc.jwt.JwtService +import org.mbari.oni.jpa.DatabaseFunSuite +import org.mbari.oni.jpa.entities.TestEntityFactory +import org.mbari.oni.services.UserAccountService +import sttp.client3.* +import sttp.client3.circe.* +import sttp.client3.testing.SttpBackendStub +import sttp.model.StatusCode +import sttp.tapir.server.interceptor.CustomiseInterceptors +import sttp.tapir.server.interceptor.exception.ExceptionHandler +import sttp.tapir.server.model.ValuedEndpointOutput +import sttp.tapir.server.nima.{Id, NimaServerOptions} +import sttp.tapir.server.stub.TapirStubInterpreter + +import java.util.Base64 + + +trait AuthorizationEndpointsSuite extends DatabaseFunSuite with EndpointsSuite: + + given jwtService: JwtService = JwtService("mbari", "foo", "bar") + lazy val authorizationEndpoints = new AuthorizationEndpoints(entityManagerFactory) + + test("auth"): + + val backendStub = newBackendStub(authorizationEndpoints.authEndpointImpl) + + val response = basicRequest + .post(uri"http://test.com/v1/auth") + .header("Authorization", "APIKEY foo") + .send(backendStub) + + response.body match + case Left(e) => fail(e) + case Right(body) => + + assertEquals(response.code, StatusCode.Ok) + assert(response.body.isRight) + + // println(body) + val d = decode[Authorization](body) + assert(d.isRight) + val bearerAuth = d.getOrElse(throw new Exception("No bearer auth")) + assert(jwtService.verify(bearerAuth.accessToken)) + + test("login"): + val userService = UserAccountService(entityManagerFactory) + val userAccount = UserAccount( + "test1234", + "SuperSecretPassword", + UserAccountRoles.ADMINISTRATOR.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) + + response.body match + case Left(e) => + fail(e) + case Right(body) => + + assertEquals(response.code, StatusCode.Ok) + assert(response.body.isRight) + + // println(body) + val d = decode[Authorization](body) + assert(d.isRight) + val bearerAuth = d.getOrElse(throw new Exception("No bearer auth")) + assert(jwtService.verify(bearerAuth.accessToken)) + + diff --git a/it/src/main/scala/org/mbari/oni/jpa/entities/TestEntityFactory.scala b/it/src/main/scala/org/mbari/oni/jpa/entities/TestEntityFactory.scala index 6ed7aec..65556f6 100644 --- a/it/src/main/scala/org/mbari/oni/jpa/entities/TestEntityFactory.scala +++ b/it/src/main/scala/org/mbari/oni/jpa/entities/TestEntityFactory.scala @@ -16,7 +16,7 @@ package org.mbari.oni.jpa.entities -import org.mbari.oni.domain.{ConceptNameTypes, MediaTypes} +import org.mbari.oni.domain.{ConceptNameTypes, MediaTypes, UserAccountRoles} import org.mbari.oni.etc.jdk.Strings import java.time.Instant @@ -151,3 +151,14 @@ object TestEntityFactory: // DON'T DO THIS. The ID should be assigned by the database. Otherwise inserts will fail. // entity.setId(nextConceptId.incrementAndGet()) entity + + def createUserAccount(role: String = UserAccountRoles.ADMINISTRATOR.getRoleName): UserAccountEntity = + val entity = new UserAccountEntity() + entity.setUserName(Strings.random(20)) + entity.setPassword(Strings.random(20)) + entity.setEmail(s"{Strings.random(10)}@mbari.org") + entity.setFirstName(Strings.random(10)) + entity.setLastName(Strings.random(10)) + entity.setAffiliation(Strings.random(20)) + entity.setRole(role) + entity diff --git a/oni/src/main/scala/org/mbari/oni/etc/jwt/JwtService.scala b/oni/src/main/scala/org/mbari/oni/etc/jwt/JwtService.scala index b5776de..e082c93 100644 --- a/oni/src/main/scala/org/mbari/oni/etc/jwt/JwtService.scala +++ b/oni/src/main/scala/org/mbari/oni/etc/jwt/JwtService.scala @@ -78,6 +78,7 @@ case class JwtService(issuer: String, apiKey: String, signingSecret: String): .withIssuer(issuer) .withIssuedAt(iat) .withExpiresAt(exp) + .withSubject(Option(entity.getId).getOrElse(-1).toString) .withClaim("name", name) .withClaim("role", entity.getRole) .sign(algorithm) diff --git a/oni/src/test/scala/org/mbari/oni/endpoints/AuthorizationEndpointsSuite.scala b/oni/src/test/scala/org/mbari/oni/endpoints/AuthorizationEndpointsSuite.scala deleted file mode 100644 index 36658c1..0000000 --- a/oni/src/test/scala/org/mbari/oni/endpoints/AuthorizationEndpointsSuite.scala +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.endpoints - -import io.circe.parser.decode -import org.mbari.oni.domain.Authorization -import org.mbari.oni.etc.circe.CirceCodecs.given -import org.mbari.oni.etc.jwt.JwtService -import sttp.client3.SttpBackend -import sttp.client3.* -import sttp.client3.circe.* -import sttp.client3.testing.SttpBackendStub -import sttp.model.StatusCode -import sttp.tapir.server.interceptor.CustomiseInterceptors -import sttp.tapir.server.interceptor.exception.ExceptionHandler -import sttp.tapir.server.model.ValuedEndpointOutput -import sttp.tapir.server.stub.TapirStubInterpreter -import sttp.tapir.server.nima.NimaServerOptions -import sttp.tapir.server.nima.Id - - -class AuthorizationEndpointsSuite extends munit.FunSuite: - - given jwtService: JwtService = new JwtService("mbari", "foo", "bar") - val authorizationEndpoints = new AuthorizationEndpoints() - - test("auth"): - - // --- START: This block adds exception logging to the stub - val exceptionHandler = ExceptionHandler.pure[Id](ctx => - Some( - ValuedEndpointOutput( - sttp.tapir.stringBody.and(sttp.tapir.statusCode), - (s"failed due to ${ctx.e.getMessage}", StatusCode.InternalServerError) - ) - ) - ) - - val customOptions: CustomiseInterceptors[Id, NimaServerOptions] = - NimaServerOptions - .customiseInterceptors - .exceptionHandler(exceptionHandler) - // --- END: This block adds exception logging to the stub - - val backendStub: SttpBackend[Id, Any] = - TapirStubInterpreter(customOptions, SttpBackendStub.synchronous) - .whenServerEndpointRunLogic(authorizationEndpoints.authEndpointImpl) - .backend() - - val response = basicRequest - .post(uri"http://test.com/v1/auth") - .header("Authorization", "APIKEY foo") - .send(backendStub) - - response.body match - case Left(e) => fail(e) - case Right(body) => - - assertEquals(response.code, StatusCode.Ok) - assert(response.body.isRight) - - // println(body) - val d = decode[Authorization](body) - assert(d.isRight) - val bearerAuth = d.getOrElse(throw new Exception("No bearer auth")) - assert(jwtService.verify(bearerAuth.accessToken)) - -