mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-08-22 02:02:14 +00:00
Replace the repos in the portal with dynamodb and core (#206)
Replace the repos in the portal with dynamodb and core * Remove all data stores from the portal * Use the user and user change repository from core and dynamodb * Remove the UserAccount type, use core User instead * Remove the UserChangeLog types, use core UserChange instead * Clean up duplication in VinylDNS * Moved `Module` to `modules.VinylDNSModule`. The reason is that you cannot disable the "default" module for unit tests. * Use mock configuration for VinylDNSSpec and FrontendControllerSpec. The mock app configuration is what allows us to run without dynamodb * Added a TestApplicationData trait to cut down on duplication
This commit is contained in:
parent
b9bc20870e
commit
0f2fdc9c7b
@ -73,7 +73,7 @@ lazy val testSettings = Seq(
|
||||
parallelExecution in Test := false,
|
||||
parallelExecution in IntegrationTest := false,
|
||||
fork in IntegrationTest := false,
|
||||
testOptions in Test += Tests.Argument("-oD"),
|
||||
testOptions in Test += Tests.Argument("-oDNCXEHPQRMIK"),
|
||||
logBuffered in Test := false
|
||||
)
|
||||
|
||||
@ -315,7 +315,7 @@ lazy val portal = (project in file("modules/portal")).enablePlugins(PlayScala, A
|
||||
// change the name of the output to portal.zip
|
||||
packageName in Universal := "portal"
|
||||
)
|
||||
.dependsOn(core)
|
||||
.dependsOn(core, dynamodb)
|
||||
|
||||
lazy val docSettings = Seq(
|
||||
git.remoteRepo := "https://github.com/vinyldns/vinyldns",
|
||||
|
@ -18,6 +18,7 @@ package vinyldns.core.domain.membership
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils
|
||||
import org.joda.time.DateTime
|
||||
import vinyldns.core.domain.membership.LockStatus.LockStatus
|
||||
|
||||
@ -26,7 +27,7 @@ object LockStatus extends Enumeration {
|
||||
val Locked, Unlocked = Value
|
||||
}
|
||||
|
||||
case class User(
|
||||
final case class User(
|
||||
userName: String,
|
||||
accessKey: String,
|
||||
secretKey: String,
|
||||
@ -41,4 +42,11 @@ case class User(
|
||||
|
||||
def updateUserLockStatus(lockStatus: LockStatus): User =
|
||||
this.copy(lockStatus = lockStatus)
|
||||
|
||||
def regenerateCredentials(): User =
|
||||
copy(accessKey = User.generateKey, secretKey = User.generateKey)
|
||||
}
|
||||
|
||||
object User {
|
||||
def generateKey: String = RandomStringUtils.randomAlphanumeric(20)
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
|
||||
package vinyldns.core.domain.membership
|
||||
import java.util.UUID
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
sealed abstract class UserChangeType(val value: String)
|
||||
@ -48,14 +50,18 @@ sealed trait UserChange {
|
||||
def created: DateTime
|
||||
}
|
||||
object UserChange {
|
||||
final case class CreateUser(id: String, newUser: User, madeByUserId: String, created: DateTime)
|
||||
extends UserChange
|
||||
final case class UpdateUser(
|
||||
id: String,
|
||||
final case class CreateUser(
|
||||
newUser: User,
|
||||
madeByUserId: String,
|
||||
created: DateTime,
|
||||
oldUser: User)
|
||||
id: String = UUID.randomUUID().toString)
|
||||
extends UserChange
|
||||
final case class UpdateUser(
|
||||
newUser: User,
|
||||
madeByUserId: String,
|
||||
created: DateTime,
|
||||
oldUser: User,
|
||||
id: String = UUID.randomUUID().toString)
|
||||
extends UserChange
|
||||
|
||||
def apply(
|
||||
@ -67,10 +73,10 @@ object UserChange {
|
||||
changeType: UserChangeType): Either[IllegalArgumentException, UserChange] =
|
||||
changeType match {
|
||||
case UserChangeType.Create =>
|
||||
Right(CreateUser(id, newUser, madeByUserId, created))
|
||||
Right(CreateUser(newUser, madeByUserId, created, id))
|
||||
case UserChangeType.Update =>
|
||||
oldUser
|
||||
.map(u => Right(UpdateUser(id, newUser, madeByUserId, created, u)))
|
||||
.map(u => Right(UpdateUser(newUser, madeByUserId, created, u, id)))
|
||||
.getOrElse(Left(new IllegalArgumentException(
|
||||
s"Unable to create update user change, old user is not defined")))
|
||||
}
|
||||
|
@ -31,5 +31,7 @@ trait UserRepository extends Repository {
|
||||
|
||||
def getUserByAccessKey(accessKey: String): IO[Option[User]]
|
||||
|
||||
def getUserByName(userName: String): IO[Option[User]]
|
||||
|
||||
def save(user: User): IO[User]
|
||||
}
|
||||
|
@ -27,12 +27,12 @@ class UserChangeSpec extends WordSpec with Matchers with EitherMatchers with Eit
|
||||
"apply" should {
|
||||
"succeed for CreateUser" in {
|
||||
val result = UserChange("foo", newUser, "bar", currentDate, None, UserChangeType.Create)
|
||||
result shouldBe Right(UserChange.CreateUser("foo", newUser, "bar", currentDate))
|
||||
result shouldBe Right(UserChange.CreateUser(newUser, "bar", currentDate, "foo"))
|
||||
}
|
||||
"succeed for UpdateUser" in {
|
||||
val result =
|
||||
UserChange("foo", newUser, "bar", currentDate, Some(newUser), UserChangeType.Update)
|
||||
result shouldBe Right(UserChange.UpdateUser("foo", newUser, "bar", currentDate, newUser))
|
||||
result shouldBe Right(UserChange.UpdateUser(newUser, "bar", currentDate, newUser, "foo"))
|
||||
}
|
||||
"fail for invalid parameters" in {
|
||||
val result = UserChange("foo", newUser, "bar", currentDate, None, UserChangeType.Update)
|
||||
|
@ -54,7 +54,7 @@ class DynamoDBUserChangeRepositoryIntegrationSpec extends DynamoDBIntegrationSpe
|
||||
"DynamoDBUserChangeRepository" should {
|
||||
"save a user change" in {
|
||||
val auth = AuthPrincipal(testUser, Seq.empty)
|
||||
val c = UserChange.CreateUser("foo", testUser, auth.userId, DateTime.now)
|
||||
val c = UserChange.CreateUser(testUser, auth.userId, DateTime.now, "foo")
|
||||
|
||||
val t = for {
|
||||
_ <- repo.save(c)
|
||||
@ -67,7 +67,7 @@ class DynamoDBUserChangeRepositoryIntegrationSpec extends DynamoDBIntegrationSpe
|
||||
"save a change for a modified user" in {
|
||||
val auth = AuthPrincipal(testUser, Seq.empty)
|
||||
val updated = testUser.copy(userName = testUser.userName + "-updated")
|
||||
val c = UserChange.UpdateUser("foo", updated, auth.userId, DateTime.now, testUser)
|
||||
val c = UserChange.UpdateUser(updated, auth.userId, DateTime.now, testUser, "foo")
|
||||
|
||||
val t = for {
|
||||
_ <- repo.save(c)
|
||||
|
@ -72,7 +72,7 @@ object DynamoDBUserChangeRepository {
|
||||
new AttributeValue().withM(DynamoDBUserRepository.toItem(crypto, change.newUser)))
|
||||
|
||||
change match {
|
||||
case UserChange.UpdateUser(_, _, _, _, oldUser) =>
|
||||
case UserChange.UpdateUser(_, _, _, oldUser, _) =>
|
||||
item.put(
|
||||
OLD_USER,
|
||||
new AttributeValue().withM(DynamoDBUserRepository.toItem(crypto, oldUser)))
|
||||
|
@ -161,6 +161,30 @@ class DynamoDBUserRepository private[repository] (
|
||||
.value
|
||||
}
|
||||
|
||||
def getUserByName(username: String): IO[Option[User]] = {
|
||||
val attributeNames = new util.HashMap[String, String]()
|
||||
attributeNames.put("#uname", USER_NAME)
|
||||
val attributeValues = new util.HashMap[String, AttributeValue]()
|
||||
attributeValues.put(":uname", new AttributeValue().withS(username))
|
||||
val request = new QueryRequest()
|
||||
.withTableName(userTableName)
|
||||
.withKeyConditionExpression("#uname = :uname")
|
||||
.withExpressionAttributeNames(attributeNames)
|
||||
.withExpressionAttributeValues(attributeValues)
|
||||
.withIndexName(USER_NAME_INDEX_NAME)
|
||||
|
||||
// the question is what to do with duplicate usernames, in the portal we just log loudly, staying the same here
|
||||
dynamoDBHelper.query(request).flatMap { result =>
|
||||
result.getItems.asScala.toList match {
|
||||
case x :: Nil => fromItem(x).map(Some(_))
|
||||
case Nil => IO.pure(None)
|
||||
case x :: _ =>
|
||||
log.error(s"Inconsistent data, multiple user records found for user name '$username'")
|
||||
fromItem(x).map(Some(_))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getUsers(
|
||||
userIds: Set[String],
|
||||
exclusiveStartKey: Option[String],
|
||||
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import com.amazonaws.auth.{AWSStaticCredentialsProvider, BasicAWSCredentials}
|
||||
import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration
|
||||
import com.amazonaws.services.dynamodbv2.{AmazonDynamoDBClient, AmazonDynamoDBClientBuilder}
|
||||
import com.google.inject.AbstractModule
|
||||
import controllers._
|
||||
import controllers.datastores.{
|
||||
DynamoDBChangeLogStore,
|
||||
DynamoDBUserAccountStore,
|
||||
InMemoryChangeLogStore,
|
||||
InMemoryUserAccountStore
|
||||
}
|
||||
import play.api.{Configuration, Environment}
|
||||
import vinyldns.core.crypto.CryptoAlgebra
|
||||
|
||||
class Module(environment: Environment, configuration: Configuration) extends AbstractModule {
|
||||
|
||||
val settings = new Settings(configuration)
|
||||
|
||||
def configure(): Unit = {
|
||||
val crypto = CryptoAlgebra.load(configuration.underlying.getConfig("crypto")).unsafeRunSync()
|
||||
bind(classOf[Authenticator]).toInstance(authenticator())
|
||||
bind(classOf[UserAccountStore]).toInstance(userAccountStore(crypto))
|
||||
bind(classOf[ChangeLogStore]).toInstance(changeLogStore(crypto))
|
||||
}
|
||||
|
||||
private def authenticator(): Authenticator =
|
||||
/**
|
||||
* Why not load config here you ask? Well, there is some ugliness in the LdapAuthenticator
|
||||
* that I am not looking to undo at this time. There are private classes
|
||||
* that do some wrapping. It all seems to work, so I am leaving it alone
|
||||
* to complete the Play framework upgrade
|
||||
*/
|
||||
LdapAuthenticator(settings)
|
||||
|
||||
private def userAccountStore(crypto: CryptoAlgebra) = {
|
||||
val useDummy = configuration.get[Boolean]("users.dummy")
|
||||
if (useDummy)
|
||||
new InMemoryUserAccountStore
|
||||
else {
|
||||
// Important! For some reason the basic credentials get lost in Jenkins. Set the aws system properties
|
||||
// just in case
|
||||
val dynamoAKID = configuration.get[String]("dynamo.key")
|
||||
val dynamoSecret = configuration.get[String]("dynamo.secret")
|
||||
val dynamoEndpoint = configuration.get[String]("dynamo.endpoint")
|
||||
val region = configuration.get[String]("dynamo.region")
|
||||
val credentials = new BasicAWSCredentials(dynamoAKID, dynamoSecret)
|
||||
val dynamoClient = AmazonDynamoDBClientBuilder
|
||||
.standard()
|
||||
.withCredentials(new AWSStaticCredentialsProvider(credentials))
|
||||
.withEndpointConfiguration(new EndpointConfiguration(dynamoEndpoint, region))
|
||||
.build()
|
||||
.asInstanceOf[AmazonDynamoDBClient]
|
||||
new DynamoDBUserAccountStore(dynamoClient, configuration, crypto)
|
||||
}
|
||||
}
|
||||
|
||||
private def changeLogStore(crypto: CryptoAlgebra) = {
|
||||
val useDummy = configuration.get[Boolean]("changelog.dummy")
|
||||
if (useDummy)
|
||||
new InMemoryChangeLogStore
|
||||
else {
|
||||
val dynamoAKID = configuration.get[String]("dynamo.key")
|
||||
val dynamoSecret = configuration.get[String]("dynamo.secret")
|
||||
val dynamoEndpoint = configuration.get[String]("dynamo.endpoint")
|
||||
val region = configuration.get[String]("dynamo.region")
|
||||
val credentials = new BasicAWSCredentials(dynamoAKID, dynamoSecret)
|
||||
val dynamoClient = AmazonDynamoDBClientBuilder
|
||||
.standard()
|
||||
.withCredentials(new AWSStaticCredentialsProvider(credentials))
|
||||
.withEndpointConfiguration(new EndpointConfiguration(dynamoEndpoint, region))
|
||||
.build()
|
||||
.asInstanceOf[AmazonDynamoDBClient]
|
||||
new DynamoDBChangeLogStore(dynamoClient, configuration, crypto)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import models.UserAccount
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
// $COVERAGE-OFF$
|
||||
object ChangeType {
|
||||
def apply(s: String): ChangeType =
|
||||
s.toLowerCase match {
|
||||
case "created" => Create
|
||||
case "updated" => Update
|
||||
case "deleted" => Delete
|
||||
case _ => throw new IllegalArgumentException(s"$s is not a valid change type")
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait ChangeType
|
||||
case object Create extends ChangeType {
|
||||
override val toString = "created"
|
||||
}
|
||||
case object Update extends ChangeType {
|
||||
override val toString = "updated"
|
||||
}
|
||||
case object Delete extends ChangeType {
|
||||
override val toString = "deleted"
|
||||
}
|
||||
|
||||
sealed trait ChangeLogMessage
|
||||
final case class UserChangeMessage(
|
||||
userId: String,
|
||||
username: String,
|
||||
timeStamp: DateTime,
|
||||
changeType: ChangeType,
|
||||
updatedUser: UserAccount,
|
||||
previousUser: Option[UserAccount])
|
||||
extends ChangeLogMessage
|
||||
|
||||
trait ChangeLogStore {
|
||||
def log(change: ChangeLogMessage): Try[ChangeLogMessage]
|
||||
}
|
||||
// $COVERAGE-ON$
|
@ -16,24 +16,13 @@
|
||||
|
||||
package controllers
|
||||
|
||||
import cats.effect.IO
|
||||
import javax.inject.{Inject, Singleton}
|
||||
|
||||
import models.UserAccount
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
case class UserChangeRecord(
|
||||
changeId: Long,
|
||||
userId: String,
|
||||
user: String,
|
||||
timeStamp: DateTime,
|
||||
changeType: UserChangeType,
|
||||
newUser: UserAccount,
|
||||
oldUser: UserAccount)
|
||||
import vinyldns.core.domain.membership.{User, UserChange, UserChangeRepository, UserRepository}
|
||||
|
||||
@Singleton
|
||||
class UserAccountAccessor @Inject()(store: UserAccountStore) {
|
||||
class UserAccountAccessor @Inject()(users: UserRepository, changes: UserChangeRepository) {
|
||||
|
||||
/**
|
||||
* Lookup a user in the store. Using identifier as the user id and/or name
|
||||
@ -42,16 +31,24 @@ class UserAccountAccessor @Inject()(store: UserAccountStore) {
|
||||
* @return Success(Some(user account)) on success, Success(None) if the user does not exist and Failure when there
|
||||
* was an error.
|
||||
*/
|
||||
def get(identifier: String): Try[Option[UserAccount]] =
|
||||
store.getUserById(identifier) match {
|
||||
case Success(None) => store.getUserByName(identifier)
|
||||
case Success(Some(user)) => Success(Some(user))
|
||||
case Failure(ex) => Failure(ex)
|
||||
def get(identifier: String): IO[Option[User]] =
|
||||
users.getUser(identifier).flatMap {
|
||||
case None => users.getUserByName(identifier)
|
||||
case found => IO(found)
|
||||
}
|
||||
|
||||
def put(user: UserAccount): Try[UserAccount] =
|
||||
store.storeUser(user)
|
||||
def create(user: User): IO[User] =
|
||||
for {
|
||||
_ <- users.save(user)
|
||||
_ <- changes.save(UserChange.CreateUser(user, "system", DateTime.now))
|
||||
} yield user
|
||||
|
||||
def getUserByKey(key: String): Try[Option[UserAccount]] =
|
||||
store.getUserByKey(key)
|
||||
def update(user: User, oldUser: User): IO[User] =
|
||||
for {
|
||||
_ <- users.save(user)
|
||||
_ <- changes.save(UserChange.UpdateUser(user, "system", DateTime.now, oldUser))
|
||||
} yield user
|
||||
|
||||
def getUserByKey(key: String): IO[Option[User]] =
|
||||
users.getUserByAccessKey(key)
|
||||
}
|
||||
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import models.UserAccount
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
// $COVERAGE-OFF$
|
||||
|
||||
trait UserAccountStore {
|
||||
def getUserById(userId: String): Try[Option[UserAccount]]
|
||||
def getUserByName(username: String): Try[Option[UserAccount]]
|
||||
def getUserByKey(accessKey: String): Try[Option[UserAccount]]
|
||||
def storeUser(user: UserAccount): Try[UserAccount]
|
||||
}
|
||||
|
||||
sealed trait UserChangeType
|
||||
final case object Created extends UserChangeType
|
||||
final case object Updated extends UserChangeType
|
||||
final case object Deleted extends UserChangeType
|
||||
// $COVERAGE-ON$
|
@ -19,7 +19,7 @@ package controllers
|
||||
import java.util
|
||||
|
||||
import com.amazonaws.auth.{AWSCredentials, BasicAWSCredentials, SignerFactory}
|
||||
import models.{SignableVinylDNSRequest, UserAccount, VinylDNSRequest}
|
||||
import models.{SignableVinylDNSRequest, VinylDNSRequest}
|
||||
import org.joda.time.DateTime
|
||||
import play.api.{Logger, _}
|
||||
import play.api.data.Form
|
||||
@ -28,7 +28,11 @@ import play.api.libs.json._
|
||||
import play.api.libs.ws.WSClient
|
||||
import play.api.mvc._
|
||||
import java.util.HashMap
|
||||
|
||||
import cats.effect.IO
|
||||
import javax.inject.{Inject, Singleton}
|
||||
import vinyldns.core.domain.membership.LockStatus.LockStatus
|
||||
import vinyldns.core.domain.membership.{LockStatus, User}
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
@ -44,8 +48,6 @@ object VinylDNS {
|
||||
private val MSG = "alertMessage"
|
||||
def error(msg: String): Flash = Flash(Map(TYPE -> "danger", MSG -> msg))
|
||||
def warning(msg: String): Flash = Flash(Map(TYPE -> "warning", MSG -> msg))
|
||||
def info(msg: String): Flash = Flash(Map(TYPE -> "info", MSG -> msg))
|
||||
def success(msg: String): Flash = Flash(Map(TYPE -> "success", MSG -> msg))
|
||||
|
||||
def fromFlash(flash: Flash): Option[Alert] =
|
||||
(flash.get(TYPE), flash.get(MSG)) match {
|
||||
@ -63,14 +65,14 @@ object VinylDNS {
|
||||
isSuper: Boolean,
|
||||
id: String)
|
||||
object UserInfo {
|
||||
def fromAccount(account: UserAccount): UserInfo =
|
||||
def fromUser(user: User): UserInfo =
|
||||
UserInfo(
|
||||
userName = account.username,
|
||||
firstName = account.firstName,
|
||||
lastName = account.lastName,
|
||||
email = account.email,
|
||||
isSuper = account.isSuper,
|
||||
id = account.userId
|
||||
userName = user.userName,
|
||||
firstName = user.firstName,
|
||||
lastName = user.lastName,
|
||||
email = user.email,
|
||||
isSuper = user.isSuper,
|
||||
id = user.id
|
||||
)
|
||||
}
|
||||
|
||||
@ -93,7 +95,6 @@ class VinylDNS @Inject()(
|
||||
configuration: Configuration,
|
||||
authenticator: Authenticator,
|
||||
userAccountAccessor: UserAccountAccessor,
|
||||
auditLogAccessor: ChangeLogStore,
|
||||
wsClient: WSClient,
|
||||
components: ControllerComponents)
|
||||
extends AbstractController(components) {
|
||||
@ -205,27 +206,22 @@ class VinylDNS @Inject()(
|
||||
withAuthenticatedUser { user =>
|
||||
val response = userAccountAccessor.get(user).map {
|
||||
case Some(userDetails) =>
|
||||
Ok(Json.toJson(VinylDNS.UserInfo.fromAccount(userDetails)))
|
||||
Ok(Json.toJson(VinylDNS.UserInfo.fromUser(userDetails)))
|
||||
.withHeaders(cacheHeaders: _*)
|
||||
case _ =>
|
||||
Status(404)(s"Did not find user data for '$user'")
|
||||
}
|
||||
Future.fromTry(response)
|
||||
response.unsafeToFuture()
|
||||
}
|
||||
}
|
||||
|
||||
private def processCsv(username: String, account: UserAccount): Result =
|
||||
account.username match {
|
||||
private def processCsv(username: String, user: User): Result =
|
||||
user.userName match {
|
||||
case accountUsername: String if accountUsername == username =>
|
||||
Logger.info(
|
||||
s"Sending credentials for user=$username with key accessKey=${account.accessKey}")
|
||||
Logger.info(s"Sending credentials for user=$username with key accessKey=${user.accessKey}")
|
||||
Ok(
|
||||
s"NT ID, access key, secret key,api url\n%s,%s,%s,%s"
|
||||
.format(
|
||||
account.username,
|
||||
account.accessKey,
|
||||
account.accessSecret,
|
||||
vinyldnsServiceBackend))
|
||||
.format(user.userName, user.accessKey, user.secretKey, vinyldnsServiceBackend))
|
||||
.as("text/csv")
|
||||
|
||||
case _ =>
|
||||
@ -236,12 +232,15 @@ class VinylDNS @Inject()(
|
||||
def serveCredsFile(fileName: String): Action[AnyContent] = Action.async { implicit request =>
|
||||
Logger.info(s"Serving credentials for file $fileName")
|
||||
withAuthenticatedUser { username =>
|
||||
userAccountAccessor.get(username) match {
|
||||
case Success(Some(account)) => Future(processCsv(username, account))
|
||||
case Success(None) =>
|
||||
throw new UnsupportedOperationException(s"Error - User account for $username not found")
|
||||
case Failure(ex) => throw ex
|
||||
}
|
||||
userAccountAccessor
|
||||
.get(username)
|
||||
.flatMap {
|
||||
case Some(account) => IO(processCsv(username, account))
|
||||
case None =>
|
||||
IO.raiseError(
|
||||
new UnsupportedOperationException(s"Error - User account for $username not found"))
|
||||
}
|
||||
.unsafeToFuture()
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,7 +251,7 @@ class VinylDNS @Inject()(
|
||||
.map(response => {
|
||||
Status(200)("Successfully regenerated credentials")
|
||||
.withHeaders(cacheHeaders: _*)
|
||||
.withSession("username" -> response.username, "accessKey" -> response.accessKey)
|
||||
.withSession("username" -> response.userName, "accessKey" -> response.accessKey)
|
||||
})
|
||||
.recover {
|
||||
case _: UserDoesNotExistException =>
|
||||
@ -261,62 +260,50 @@ class VinylDNS @Inject()(
|
||||
}
|
||||
}
|
||||
|
||||
private def processRegenerate(oldAccountName: String): Try[UserAccount] =
|
||||
for {
|
||||
oldAccount <- userAccountAccessor.get(oldAccountName).flatMap {
|
||||
case Some(account) => Success(account)
|
||||
private def processRegenerate(oldAccountName: String): Try[User] = {
|
||||
val update = for {
|
||||
oldUser <- userAccountAccessor.get(oldAccountName).flatMap {
|
||||
case Some(u) => IO.pure(u)
|
||||
case None =>
|
||||
Failure(
|
||||
IO.raiseError(
|
||||
new UserDoesNotExistException(s"Error - User account for $oldAccountName not found"))
|
||||
}
|
||||
account = oldAccount.regenerateCredentials()
|
||||
_ <- userAccountAccessor.put(account)
|
||||
_ <- auditLogAccessor.log(
|
||||
UserChangeMessage(
|
||||
account.userId,
|
||||
account.username,
|
||||
DateTime.now(),
|
||||
ChangeType("updated"),
|
||||
account,
|
||||
Some(oldAccount)))
|
||||
newUser = oldUser.regenerateCredentials()
|
||||
_ <- userAccountAccessor.update(newUser, oldUser)
|
||||
} yield {
|
||||
Logger.info(s"Credentials successfully regenerated for ${account.username}")
|
||||
account
|
||||
}
|
||||
|
||||
private def createNewUser(details: UserDetails): Try[UserAccount] = {
|
||||
val newAccount =
|
||||
UserAccount(details.username, details.firstName, details.lastName, details.email)
|
||||
for {
|
||||
newUser <- userAccountAccessor.put(newAccount)
|
||||
} yield {
|
||||
auditLogAccessor.log(
|
||||
UserChangeMessage(
|
||||
newUser.userId,
|
||||
newUser.username,
|
||||
DateTime.now(),
|
||||
ChangeType("created"),
|
||||
newUser,
|
||||
None))
|
||||
Logger.info(s"Credentials successfully regenerated for ${newUser.userName}")
|
||||
newUser
|
||||
}
|
||||
|
||||
update.attempt.unsafeRunSync().toTry
|
||||
}
|
||||
|
||||
private def createNewUser(details: UserDetails): IO[User] = {
|
||||
val newUser =
|
||||
User(
|
||||
details.username,
|
||||
User.generateKey,
|
||||
User.generateKey,
|
||||
details.firstName,
|
||||
details.lastName,
|
||||
details.email)
|
||||
userAccountAccessor.create(newUser)
|
||||
}
|
||||
|
||||
def getUserDataByUsername(username: String): Action[AnyContent] = Action.async {
|
||||
implicit request =>
|
||||
withAuthenticatedUser { _ =>
|
||||
Future
|
||||
.fromTry {
|
||||
for {
|
||||
userDetails <- authenticator.lookup(username)
|
||||
existingAccount <- userAccountAccessor.get(userDetails.username)
|
||||
userAccount <- existingAccount match {
|
||||
case Some(user) => Try(VinylDNS.UserInfo.fromAccount(user))
|
||||
case None =>
|
||||
createNewUser(userDetails).map(VinylDNS.UserInfo.fromAccount)
|
||||
}
|
||||
} yield userAccount
|
||||
}
|
||||
{
|
||||
for {
|
||||
userDetails <- IO.fromEither(authenticator.lookup(username).toEither)
|
||||
existingAccount <- userAccountAccessor.get(userDetails.username)
|
||||
userAccount <- existingAccount match {
|
||||
case Some(user) => IO(VinylDNS.UserInfo.fromUser(user))
|
||||
case None =>
|
||||
createNewUser(userDetails).map(VinylDNS.UserInfo.fromUser)
|
||||
}
|
||||
} yield userAccount
|
||||
}.unsafeToFuture()
|
||||
.map(Json.toJson(_))
|
||||
.map(Ok(_).withHeaders(cacheHeaders: _*))
|
||||
.recover {
|
||||
@ -335,33 +322,24 @@ class VinylDNS @Inject()(
|
||||
Logger.info(
|
||||
s"user [${userDetails.username}] logged in with ldap path [${userDetails.nameInNamespace}]")
|
||||
|
||||
// get or create the new style user account
|
||||
val userAccount = userAccountAccessor
|
||||
val user = userAccountAccessor
|
||||
.get(userDetails.username)
|
||||
.flatMap {
|
||||
case None =>
|
||||
Logger.info(s"Creating user account for ${userDetails.username}")
|
||||
createNewUser(userDetails).map {
|
||||
case user: UserAccount =>
|
||||
Logger.info(s"User account for ${user.username} created with id ${user.userId}")
|
||||
user
|
||||
createNewUser(userDetails).map { u: User =>
|
||||
Logger.info(s"User account for ${u.userName} created with id ${u.id}")
|
||||
u
|
||||
}
|
||||
case Some(user) =>
|
||||
Logger.info(s"User account for ${user.username} exists with id ${user.userId}")
|
||||
Success(user)
|
||||
case Some(u) =>
|
||||
Logger.info(s"User account for ${u.userName} exists with id ${u.id}")
|
||||
IO.pure(u)
|
||||
}
|
||||
.recoverWith {
|
||||
case ex =>
|
||||
Logger.error(
|
||||
s"User retrieval or creation failed for user ${userDetails.username} with message ${ex.getMessage}")
|
||||
throw ex
|
||||
}
|
||||
.get
|
||||
.unsafeRunSync()
|
||||
|
||||
Logger.info(
|
||||
s"--NEW MEMBERSHIP-- user [${userAccount.username}] logged in with id [${userAccount.userId}]")
|
||||
Logger.info(s"--NEW MEMBERSHIP-- user [${user.userName}] logged in with id [${user.id}]")
|
||||
Redirect("/index")
|
||||
.withSession("username" -> userAccount.username, "accessKey" -> userAccount.accessKey)
|
||||
.withSession("username" -> user.userName, "accessKey" -> user.accessKey)
|
||||
}
|
||||
|
||||
def getZones: Action[AnyContent] = Action.async { implicit request =>
|
||||
@ -564,13 +542,13 @@ class VinylDNS @Inject()(
|
||||
def getUserCreds(keyOption: Option[String]): BasicAWSCredentials =
|
||||
keyOption match {
|
||||
case Some(key) =>
|
||||
userAccountAccessor.getUserByKey(key) match {
|
||||
case Success(Some(account)) =>
|
||||
new BasicAWSCredentials(account.accessKey, account.accessSecret)
|
||||
case Success(None) =>
|
||||
userAccountAccessor.getUserByKey(key).attempt.unsafeRunSync() match {
|
||||
case Right(Some(account)) =>
|
||||
new BasicAWSCredentials(account.accessKey, account.secretKey)
|
||||
case Right(None) =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Key [$key] Not Found!! Please logout then back in.")
|
||||
case Failure(ex) => throw ex
|
||||
case Left(ex) => throw ex
|
||||
}
|
||||
case None => throw new IllegalArgumentException("No Key Found!!")
|
||||
}
|
||||
|
@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers.datastores
|
||||
|
||||
import java.util
|
||||
|
||||
import com.amazonaws.AmazonClientException
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient
|
||||
import com.amazonaws.services.dynamodbv2.model._
|
||||
import controllers.{ChangeLogMessage, ChangeLogStore, UserChangeMessage}
|
||||
import play.api.Configuration
|
||||
import vinyldns.core.crypto.CryptoAlgebra
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
class DynamoDBChangeLogStore(
|
||||
dynamo: AmazonDynamoDBClient,
|
||||
config: Configuration,
|
||||
crypto: CryptoAlgebra)
|
||||
extends ChangeLogStore {
|
||||
|
||||
private val tableName = config.get[String]("changelog.tablename")
|
||||
private val readThroughput =
|
||||
config.getOptional[Long]("changelog.provisionedReadThroughput").getOrElse(1L)
|
||||
private val writeThroughput =
|
||||
config.getOptional[Long]("changelog.provisionedWriteThroughput").getOrElse(1L)
|
||||
|
||||
private val TIME_STAMP = "timestamp"
|
||||
|
||||
private val tableAttributes = Seq(
|
||||
new AttributeDefinition(TIME_STAMP, "S")
|
||||
)
|
||||
|
||||
try {
|
||||
dynamo.describeTable(new DescribeTableRequest(tableName))
|
||||
} catch {
|
||||
case _: AmazonClientException =>
|
||||
dynamo.createTable(
|
||||
new CreateTableRequest()
|
||||
.withTableName(tableName)
|
||||
.withAttributeDefinitions(tableAttributes: _*)
|
||||
.withKeySchema(new KeySchemaElement(TIME_STAMP, KeyType.HASH))
|
||||
.withProvisionedThroughput(new ProvisionedThroughput(readThroughput, writeThroughput)))
|
||||
dynamo.describeTable(new DescribeTableRequest(tableName))
|
||||
}
|
||||
|
||||
def log(change: ChangeLogMessage): Try[ChangeLogMessage] =
|
||||
Try {
|
||||
change match {
|
||||
case ucm: UserChangeMessage =>
|
||||
dynamo.putItem(tableName, toDynamoItem(ucm))
|
||||
ucm
|
||||
}
|
||||
}
|
||||
|
||||
def toDynamoItem(message: UserChangeMessage): java.util.HashMap[String, AttributeValue] = {
|
||||
val item = new util.HashMap[String, AttributeValue]()
|
||||
item.put("timestamp", new AttributeValue(message.timeStamp.toString))
|
||||
item.put("userId", new AttributeValue(message.userId))
|
||||
item.put("username", new AttributeValue(message.username))
|
||||
item.put("changeType", new AttributeValue(message.changeType.toString))
|
||||
item.put(
|
||||
"updatedUser",
|
||||
new AttributeValue().withM(DynamoDBUserAccountStore.toItem(message.updatedUser, crypto)))
|
||||
message.previousUser match {
|
||||
case Some(user) =>
|
||||
item.put(
|
||||
"previousUser",
|
||||
new AttributeValue().withM(DynamoDBUserAccountStore.toItem(user, crypto)))
|
||||
case None => ()
|
||||
}
|
||||
item
|
||||
}
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers.datastores
|
||||
|
||||
import controllers.UserAccountStore
|
||||
import java.util
|
||||
|
||||
import com.amazonaws.AmazonClientException
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient
|
||||
import com.amazonaws.services.dynamodbv2.model._
|
||||
import models.UserAccount
|
||||
import org.joda.time.DateTime
|
||||
import play.api.Logger
|
||||
import play.api.Configuration
|
||||
import vinyldns.core.crypto.CryptoAlgebra
|
||||
|
||||
import scala.util.Try
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
object DynamoDBUserAccountStore {
|
||||
def getAttributeOrNone(
|
||||
items: java.util.Map[String, AttributeValue],
|
||||
attribute: String): Option[String] =
|
||||
Try(items.get(attribute).getS).toOption
|
||||
|
||||
def fromItem(items: java.util.Map[String, AttributeValue], crypto: CryptoAlgebra): UserAccount = {
|
||||
val superUser: Try[Boolean] = Try(items.get("super").getBOOL)
|
||||
new UserAccount(
|
||||
items.get("userid").getS,
|
||||
items.get("username").getS,
|
||||
getAttributeOrNone(items, "firstname"),
|
||||
getAttributeOrNone(items, "lastname"),
|
||||
getAttributeOrNone(items, "email"),
|
||||
new DateTime(items.get("created").getN.toLong),
|
||||
items.get("accesskey").getS,
|
||||
crypto.decrypt(items.get("secretkey").getS),
|
||||
superUser.getOrElse(false)
|
||||
)
|
||||
}
|
||||
|
||||
def toItem(ua: UserAccount, crypto: CryptoAlgebra): java.util.Map[String, AttributeValue] = {
|
||||
val item = new util.HashMap[String, AttributeValue]()
|
||||
item.put("userid", new AttributeValue().withS(ua.userId))
|
||||
item.put("username", new AttributeValue().withS(ua.username))
|
||||
ua.firstName.foreach { firstname =>
|
||||
item.put("firstname", new AttributeValue().withS(firstname))
|
||||
}
|
||||
ua.lastName.foreach { lastname =>
|
||||
item.put("lastname", new AttributeValue().withS(lastname))
|
||||
}
|
||||
ua.email.foreach { email =>
|
||||
item.put("email", new AttributeValue().withS(email))
|
||||
}
|
||||
item.put("created", new AttributeValue().withN(ua.created.getMillis.toString))
|
||||
item.put("accesskey", new AttributeValue().withS(ua.accessKey))
|
||||
item.put("secretkey", new AttributeValue().withS(crypto.encrypt(ua.accessSecret)))
|
||||
item
|
||||
}
|
||||
}
|
||||
|
||||
class DynamoDBUserAccountStore(
|
||||
dynamo: AmazonDynamoDBClient,
|
||||
config: Configuration,
|
||||
crypto: CryptoAlgebra)
|
||||
extends UserAccountStore {
|
||||
private val tableName = config.get[String]("users.tablename")
|
||||
private val readThroughput =
|
||||
config.getOptional[Long]("users.provisionedReadThroughput").getOrElse(1L)
|
||||
private val writeThroughput =
|
||||
config.getOptional[Long]("users.provisionedWriteThroughput").getOrElse(1L)
|
||||
|
||||
private val USER_ID = "userid"
|
||||
private val USER_NAME = "username"
|
||||
private val USER_INDEX_NAME = "username_index"
|
||||
private val ACCESS_KEY = "accesskey"
|
||||
private val ACCESS_KEY_INDEX_NAME = "access_key_index"
|
||||
|
||||
private val tableAttributes = Seq(
|
||||
new AttributeDefinition(USER_ID, "S"),
|
||||
new AttributeDefinition(USER_NAME, "S"),
|
||||
new AttributeDefinition(ACCESS_KEY, "S")
|
||||
)
|
||||
|
||||
private val gsis = Seq(
|
||||
new GlobalSecondaryIndex()
|
||||
.withIndexName(USER_INDEX_NAME)
|
||||
.withProvisionedThroughput(new ProvisionedThroughput(readThroughput, writeThroughput))
|
||||
.withKeySchema(new KeySchemaElement(USER_NAME, KeyType.HASH))
|
||||
.withProjection(new Projection().withProjectionType("ALL")),
|
||||
new GlobalSecondaryIndex()
|
||||
.withIndexName(ACCESS_KEY_INDEX_NAME)
|
||||
.withProvisionedThroughput(new ProvisionedThroughput(readThroughput, writeThroughput))
|
||||
.withKeySchema(new KeySchemaElement(ACCESS_KEY, KeyType.HASH))
|
||||
.withProjection(new Projection().withProjectionType("ALL"))
|
||||
)
|
||||
|
||||
try {
|
||||
dynamo.describeTable(new DescribeTableRequest(tableName))
|
||||
} catch {
|
||||
case _: AmazonClientException =>
|
||||
dynamo.createTable(
|
||||
new CreateTableRequest()
|
||||
.withTableName(tableName)
|
||||
.withAttributeDefinitions(tableAttributes: _*)
|
||||
.withKeySchema(new KeySchemaElement(USER_ID, KeyType.HASH))
|
||||
.withGlobalSecondaryIndexes(gsis: _*)
|
||||
.withProvisionedThroughput(new ProvisionedThroughput(readThroughput, writeThroughput)))
|
||||
dynamo.describeTable(new DescribeTableRequest(tableName))
|
||||
}
|
||||
|
||||
def getUserById(userId: String): Try[Option[UserAccount]] = {
|
||||
val key = new util.HashMap[String, AttributeValue]()
|
||||
key.put(USER_ID, new AttributeValue(userId))
|
||||
val request = new GetItemRequest()
|
||||
.withTableName(tableName)
|
||||
.withKey(key)
|
||||
Try {
|
||||
dynamo.getItem(request) match {
|
||||
case null => None
|
||||
// Amazon's client java docs state "If there is no matching item, GetItem does not return any data."
|
||||
// that could mean the item has no data or a null is returned, so we need to handle both cases.
|
||||
case result: GetItemResult if result.getItem == null => None
|
||||
case result: GetItemResult if result.getItem.isEmpty => None
|
||||
case result: GetItemResult =>
|
||||
Some(DynamoDBUserAccountStore.fromItem(result.getItem, crypto))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getUserByName(username: String): Try[Option[UserAccount]] = {
|
||||
val attributeNames = new util.HashMap[String, String]()
|
||||
attributeNames.put("#uname", USER_NAME)
|
||||
val attributeValues = new util.HashMap[String, AttributeValue]()
|
||||
attributeValues.put(":uname", new AttributeValue().withS(username))
|
||||
val request = new QueryRequest()
|
||||
.withTableName(tableName)
|
||||
.withKeyConditionExpression("#uname = :uname")
|
||||
.withExpressionAttributeNames(attributeNames)
|
||||
.withExpressionAttributeValues(attributeValues)
|
||||
.withIndexName(USER_INDEX_NAME)
|
||||
Try {
|
||||
dynamo.query(request) match {
|
||||
case result: QueryResult if result.getCount == 1 =>
|
||||
Some(DynamoDBUserAccountStore.fromItem(result.getItems.get(0), crypto))
|
||||
case result: QueryResult if result.getCount == 0 => None
|
||||
case result: QueryResult if result.getCount >= 2 =>
|
||||
val prefixString = "!!! INCONSISTENT DATA !!!"
|
||||
Logger.error(s"$prefixString ${result.getCount} user accounts for ntid $username found!")
|
||||
for {
|
||||
item <- result.getItems.asScala
|
||||
} {
|
||||
val user = DynamoDBUserAccountStore.fromItem(item, crypto)
|
||||
Logger.error(s"$prefixString ${user.username} has user ID of ${user.userId}")
|
||||
}
|
||||
Some(DynamoDBUserAccountStore.fromItem(result.getItems.get(0), crypto))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getUserByKey(key: String): Try[Option[UserAccount]] = {
|
||||
val attributeNames = new util.HashMap[String, String]()
|
||||
attributeNames.put("#ukey", ACCESS_KEY)
|
||||
val attributeValues = new util.HashMap[String, AttributeValue]()
|
||||
attributeValues.put(":ukey", new AttributeValue().withS(key))
|
||||
val request = new QueryRequest()
|
||||
.withTableName(tableName)
|
||||
.withKeyConditionExpression("#ukey = :ukey")
|
||||
.withExpressionAttributeNames(attributeNames)
|
||||
.withExpressionAttributeValues(attributeValues)
|
||||
.withIndexName(ACCESS_KEY_INDEX_NAME)
|
||||
Try {
|
||||
dynamo.query(request) match {
|
||||
case result: QueryResult if result.getCount == 1 =>
|
||||
Some(DynamoDBUserAccountStore.fromItem(result.getItems.get(0), crypto))
|
||||
case result: QueryResult if result.getCount == 0 => None
|
||||
case result: QueryResult if result.getCount >= 2 =>
|
||||
val prefixString = "!!! INCONSISTENT DATA !!!"
|
||||
Logger.error(s"$prefixString ${result.getCount} user accounts for access key $key found!")
|
||||
for {
|
||||
item <- result.getItems.asScala
|
||||
} {
|
||||
val user = DynamoDBUserAccountStore.fromItem(item, crypto)
|
||||
Logger.error(s"$prefixString ${user.username} has key of ${user.accessKey}")
|
||||
}
|
||||
Some(DynamoDBUserAccountStore.fromItem(result.getItems.get(0), crypto))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def storeUser(user: UserAccount): Try[UserAccount] = {
|
||||
val item = DynamoDBUserAccountStore.toItem(user, crypto)
|
||||
val request = new PutItemRequest().withItem(item).withTableName(tableName)
|
||||
|
||||
Try {
|
||||
dynamo.putItem(request) match {
|
||||
case _: PutItemResult => user
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers.datastores
|
||||
|
||||
import controllers.{ChangeLogMessage, ChangeLogStore, UserChangeMessage}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.util.Try
|
||||
|
||||
class InMemoryChangeLogStore extends ChangeLogStore {
|
||||
type InMemoryLog = mutable.MutableList[ChangeLogMessage]
|
||||
val userChangeLog = new InMemoryLog()
|
||||
|
||||
def log(change: ChangeLogMessage): Try[ChangeLogMessage] =
|
||||
Try {
|
||||
change match {
|
||||
case ucm: UserChangeMessage =>
|
||||
userChangeLog += ucm
|
||||
ucm
|
||||
}
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers.datastores
|
||||
|
||||
import controllers.UserAccountStore
|
||||
import models.UserAccount
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.util.Try
|
||||
|
||||
class InMemoryUserAccountStore extends UserAccountStore {
|
||||
val users = new mutable.HashMap[String, UserAccount]()
|
||||
val usersByNameIndex = new mutable.HashMap[String, String]()
|
||||
val usersByKeyIndex = new mutable.HashMap[String, String]()
|
||||
|
||||
def getUserById(userId: String): Try[Option[UserAccount]] =
|
||||
Try(users.get(userId))
|
||||
|
||||
def getUserByName(username: String): Try[Option[UserAccount]] =
|
||||
Try(usersByNameIndex.get(username)).flatMap {
|
||||
case Some(userId) => getUserById(userId)
|
||||
case None => Try(None)
|
||||
}
|
||||
|
||||
def getUserByKey(key: String): Try[Option[UserAccount]] =
|
||||
Try(usersByKeyIndex.get(key)).flatMap {
|
||||
case Some(userId) => getUserById(userId)
|
||||
case None => Try(None)
|
||||
}
|
||||
|
||||
def storeUser(user: UserAccount): Try[UserAccount] =
|
||||
Try {
|
||||
users.put(user.userId, user)
|
||||
usersByNameIndex.put(user.username, user.userId)
|
||||
usersByKeyIndex.put(user.accessKey, user.userId)
|
||||
user
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package models
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils
|
||||
import org.joda.time.DateTime
|
||||
|
||||
case class UserAccount(
|
||||
userId: String,
|
||||
username: String,
|
||||
firstName: Option[String],
|
||||
lastName: Option[String],
|
||||
email: Option[String],
|
||||
created: DateTime,
|
||||
accessKey: String,
|
||||
accessSecret: String,
|
||||
isSuper: Boolean = false) {
|
||||
|
||||
private def generateKey: String = RandomStringUtils.randomAlphanumeric(20)
|
||||
|
||||
override def toString() = {
|
||||
val sb = new StringBuilder
|
||||
sb.append("UserAccount: [")
|
||||
sb.append("id=\"").append(userId).append("\"; ")
|
||||
sb.append("username=\"").append(username).append("\"; ")
|
||||
sb.append("firstName=\"").append(firstName).append("\"; ")
|
||||
sb.append("lastName=\"").append(lastName).append("\"; ")
|
||||
sb.append("email=\"").append(email).append("\"; ")
|
||||
sb.append("accessKey=\"").append(accessKey).append("\"; ")
|
||||
sb.append("]")
|
||||
sb.toString
|
||||
}
|
||||
|
||||
def regenerateCredentials(): UserAccount =
|
||||
copy(accessKey = generateKey, accessSecret = generateKey)
|
||||
}
|
||||
|
||||
object UserAccount {
|
||||
private def generateKey: String = RandomStringUtils.randomAlphanumeric(20)
|
||||
|
||||
def apply(
|
||||
username: String,
|
||||
firstName: Option[String],
|
||||
lastName: Option[String],
|
||||
email: Option[String]): UserAccount = {
|
||||
val userId = UUID.randomUUID().toString
|
||||
val createdTime = DateTime.now()
|
||||
val key = generateKey
|
||||
val secret = generateKey
|
||||
|
||||
UserAccount(userId, username, firstName, lastName, email, createdTime, key, secret, false)
|
||||
}
|
||||
}
|
108
modules/portal/app/modules/VinylDNSModule.scala
Normal file
108
modules/portal/app/modules/VinylDNSModule.scala
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package modules
|
||||
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import cats.effect.IO
|
||||
import com.google.inject.AbstractModule
|
||||
import controllers._
|
||||
import play.api.{Configuration, Environment}
|
||||
import vinyldns.core.crypto.CryptoAlgebra
|
||||
import vinyldns.core.domain.membership.{UserChangeRepository, UserRepository}
|
||||
import vinyldns.dynamodb.repository.{
|
||||
DynamoDBDataStoreSettings,
|
||||
DynamoDBRepositorySettings,
|
||||
DynamoDBUserChangeRepository,
|
||||
DynamoDBUserRepository
|
||||
}
|
||||
|
||||
class VinylDNSModule(environment: Environment, configuration: Configuration)
|
||||
extends AbstractModule {
|
||||
|
||||
val settings = new Settings(configuration)
|
||||
|
||||
def configure(): Unit = {
|
||||
// Note: Leaving the unsafeRunSync here until we do full dynamic loading of the data store
|
||||
val crypto = CryptoAlgebra.load(configuration.underlying.getConfig("crypto")).unsafeRunSync()
|
||||
|
||||
val dynamoConfig = configuration.get[Configuration]("dynamo")
|
||||
val dynamoSettings = DynamoDBDataStoreSettings(
|
||||
key = dynamoConfig.get[String]("key"),
|
||||
secret = dynamoConfig.get[String]("secret"),
|
||||
endpoint = dynamoConfig.get[String]("endpoint"),
|
||||
region = dynamoConfig.get[String]("region")
|
||||
)
|
||||
|
||||
bind(classOf[Authenticator]).toInstance(authenticator())
|
||||
bind(classOf[UserRepository]).toInstance(userRepository(dynamoSettings, crypto))
|
||||
bind(classOf[UserChangeRepository]).toInstance(changeLogStore(dynamoSettings, crypto))
|
||||
}
|
||||
|
||||
private def authenticator(): Authenticator =
|
||||
/**
|
||||
* Why not load config here you ask? Well, there is some ugliness in the LdapAuthenticator
|
||||
* that I am not looking to undo at this time. There are private classes
|
||||
* that do some wrapping. It all seems to work, so I am leaving it alone
|
||||
* to complete the Play framework upgrade
|
||||
*/
|
||||
LdapAuthenticator(settings)
|
||||
|
||||
private def userRepository(
|
||||
dynamoSettings: DynamoDBDataStoreSettings,
|
||||
crypto: CryptoAlgebra): DynamoDBUserRepository = {
|
||||
for {
|
||||
repoSettings <- IO(
|
||||
DynamoDBRepositorySettings(
|
||||
tableName = configuration.get[String]("users.tablename"),
|
||||
provisionedReads = configuration.get[Long]("users.provisionedReadThroughput"),
|
||||
provisionedWrites = configuration.get[Long]("users.provisionedWriteThroughput")
|
||||
)
|
||||
)
|
||||
repo <- DynamoDBUserRepository(repoSettings, dynamoSettings, crypto)
|
||||
} yield repo
|
||||
}.unsafeRunSync()
|
||||
|
||||
private def changeLogStore(
|
||||
dynamoSettings: DynamoDBDataStoreSettings,
|
||||
crypto: CryptoAlgebra): DynamoDBUserChangeRepository = {
|
||||
for {
|
||||
repoSettings <- IO(
|
||||
DynamoDBRepositorySettings(
|
||||
tableName = configuration.get[String]("changelog.tablename"),
|
||||
provisionedReads = configuration.get[Long]("changelog.provisionedReadThroughput"),
|
||||
provisionedWrites = configuration.get[Long]("changelog.provisionedWriteThroughput")
|
||||
)
|
||||
)
|
||||
repo <- DynamoDBUserChangeRepository(repoSettings, dynamoSettings, crypto)
|
||||
} yield repo
|
||||
}.unsafeRunSync()
|
||||
}
|
@ -7,23 +7,23 @@ portal.vinyldns.backend.url = "http://not.real.com"
|
||||
dynamo {
|
||||
key = "akid goes here"
|
||||
secret = "secret key goes here"
|
||||
endpoint = "endpoint url goes here"
|
||||
endpoint = "http://foo.bar"
|
||||
region = "us-east-1" # note: we are always in us-east-1, but this can be overridden
|
||||
test_datastore = true
|
||||
test_datastore = false
|
||||
}
|
||||
|
||||
users {
|
||||
dummy = true
|
||||
tablename = "userAccounts"
|
||||
dummy = false
|
||||
tablename = "users-test"
|
||||
provisionedReadThroughput = 100
|
||||
provisionedWriteThroughput = 100
|
||||
}
|
||||
|
||||
changelog {
|
||||
dummy=true
|
||||
tablename="usersAndGroupChanges"
|
||||
provisionedReadThroughput=100
|
||||
provisionedWriteThroughput=100
|
||||
dummy = false
|
||||
tablename = "usersAndGroupChanges-test"
|
||||
provisionedReadThroughput = 100
|
||||
provisionedWriteThroughput = 100
|
||||
}
|
||||
|
||||
LDAP {
|
||||
@ -60,3 +60,5 @@ links = [
|
||||
icon = ""
|
||||
}
|
||||
]
|
||||
|
||||
play.modules.enabled += "modules.VinylDNSModule"
|
||||
|
@ -111,5 +111,7 @@ links = [
|
||||
}
|
||||
]
|
||||
|
||||
play.modules.enabled += "modules.VinylDNSModule"
|
||||
|
||||
// Local.conf has files specific to your environment, for example your own LDAP settings
|
||||
include "local.conf"
|
||||
|
@ -30,7 +30,7 @@ class ConfigSpec extends Specification {
|
||||
|
||||
dynamoAKID must beEqualTo("akid goes here")
|
||||
dynamoSecret must beEqualTo("secret key goes here")
|
||||
dynamoEndpoint must beEqualTo("endpoint url goes here")
|
||||
dynamoEndpoint must beEqualTo("http://foo.bar")
|
||||
region must beEqualTo("us-east-1")
|
||||
}
|
||||
}
|
||||
|
@ -22,12 +22,8 @@ import org.specs2.mutable.Specification
|
||||
import org.specs2.runner.JUnitRunner
|
||||
import play.api.test.Helpers._
|
||||
import play.api.test._
|
||||
import play.api.inject.guice.GuiceApplicationBuilder
|
||||
@RunWith(classOf[JUnitRunner])
|
||||
class FrontendControllerSpec extends Specification with Mockito {
|
||||
|
||||
// this has to be lazy due to how the FrontendController is boot strapped
|
||||
def app = GuiceApplicationBuilder().build()
|
||||
class FrontendControllerSpec extends Specification with Mockito with TestApplicationData {
|
||||
|
||||
"FrontendController" should {
|
||||
"send 404 on a bad request" in new WithApplication(app) {
|
||||
|
@ -18,11 +18,11 @@ package controllers
|
||||
|
||||
import javax.naming.NamingEnumeration
|
||||
import javax.naming.directory._
|
||||
|
||||
import controllers.LdapAuthenticator.{ContextCreator, LdapByDomainAuthenticator}
|
||||
import org.specs2.mock.Mockito
|
||||
import org.specs2.mock.mockito.ArgumentCapture
|
||||
import org.specs2.mutable.Specification
|
||||
import play.api.{Configuration, Environment}
|
||||
import play.api.test.WithApplication
|
||||
import play.api.inject.guice.GuiceApplicationBuilder
|
||||
|
||||
@ -63,24 +63,22 @@ class LdapAuthenticatorSpec extends Specification with Mockito {
|
||||
Mocks(contextCreator, context, searchResults, searchNext, byDomainAuthenticator, mockAttributes)
|
||||
}
|
||||
|
||||
val app = GuiceApplicationBuilder().build()
|
||||
val testApp = GuiceApplicationBuilder().configure(Map("portal.test_login" -> true)).build()
|
||||
val settings = new Settings(app.configuration)
|
||||
val testDomain1 = LdapSearchDomain("someDomain", "DC=test,DC=test,DC=com")
|
||||
val testDomain2 = LdapSearchDomain("anotherDomain", "DC=test,DC=com")
|
||||
|
||||
"LdapAuthenticator" should {
|
||||
"apply method must create an LDAP Authenticator" in new WithApplication(app) {
|
||||
val underTest = LdapAuthenticator.apply(settings)
|
||||
underTest must haveClass(
|
||||
ClassTag(
|
||||
new LdapAuthenticator(
|
||||
settings.ldapSearchBase,
|
||||
LdapByDomainAuthenticator.apply(),
|
||||
mock[ServiceAccount]).getClass))
|
||||
"apply method must create an LDAP Authenticator" in {
|
||||
val testConfig: Configuration =
|
||||
Configuration.load(Environment.simple()) ++ Configuration.from(
|
||||
Map("portal.test_login" -> false))
|
||||
val underTest = LdapAuthenticator.apply(new Settings(testConfig))
|
||||
underTest must beAnInstanceOf[LdapAuthenticator]
|
||||
}
|
||||
"apply method must create a Test Authenticator if selected" in new WithApplication(testApp) {
|
||||
val underTest = LdapAuthenticator.apply(new Settings(app.configuration))
|
||||
"apply method must create a Test Authenticator if selected" in {
|
||||
val testConfig: Configuration =
|
||||
Configuration.load(Environment.simple()) ++ Configuration.from(
|
||||
Map("portal.test_login" -> true))
|
||||
val underTest = LdapAuthenticator.apply(new Settings(testConfig))
|
||||
underTest must beAnInstanceOf[TestAuthenticator]
|
||||
}
|
||||
".authenticate" should {
|
||||
|
171
modules/portal/test/controllers/TestApplicationData.scala
Normal file
171
modules/portal/test/controllers/TestApplicationData.scala
Normal file
@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
import cats.effect.IO
|
||||
import org.joda.time.DateTime
|
||||
import org.specs2.mock.Mockito
|
||||
import play.api.inject.bind
|
||||
import play.api.inject.guice.GuiceApplicationBuilder
|
||||
import play.api.{Application, Configuration, Environment}
|
||||
import play.api.libs.json.{JsObject, JsValue, Json}
|
||||
import vinyldns.core.domain.membership._
|
||||
|
||||
import scala.util.Success
|
||||
|
||||
trait TestApplicationData { this: Mockito =>
|
||||
val frodoDetails = UserDetails(
|
||||
"CN=frodo,OU=hobbits,DC=middle,DC=earth",
|
||||
"frodo",
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
Some("Frodo"),
|
||||
Some("Baggins"))
|
||||
|
||||
val frodoUser = User(
|
||||
"fbaggins",
|
||||
"key",
|
||||
"secret",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now,
|
||||
"frodo-uuid")
|
||||
|
||||
val newFrodoLog = UserChange(
|
||||
"frodo-uuid",
|
||||
frodoUser,
|
||||
"fbaggins",
|
||||
DateTime.now,
|
||||
None,
|
||||
UserChangeType.Create
|
||||
).toOption.get
|
||||
|
||||
val serviceAccountDetails =
|
||||
UserDetails("CN=frodo,OU=hobbits,DC=middle,DC=earth", "service", None, None, None)
|
||||
val serviceAccount =
|
||||
User("service", "key", "secret", None, None, None, DateTime.now, "service-uuid")
|
||||
|
||||
val frodoJsonString: String =
|
||||
s"""{
|
||||
| "userName": "${frodoUser.userName}",
|
||||
| "firstName": "${frodoUser.firstName}",
|
||||
| "lastName": "${frodoUser.lastName}",
|
||||
| "email": "${frodoUser.email}",
|
||||
| "created": "${frodoUser.created}",
|
||||
| "id": "${frodoUser.id}"
|
||||
|}
|
||||
""".stripMargin
|
||||
|
||||
val samAccount = User(
|
||||
"sgamgee",
|
||||
"key",
|
||||
"secret",
|
||||
Some("Samwise"),
|
||||
Some("Gamgee"),
|
||||
Some("sgamgee@hobbitmail.me"),
|
||||
DateTime.now,
|
||||
"sam-uuid")
|
||||
val samDetails = UserDetails(
|
||||
"CN=sam,OU=hobbits,DC=middle,DC=earth",
|
||||
"sam",
|
||||
Some("sgamgee@hobbitmail.me"),
|
||||
Some("Sam"),
|
||||
Some("Gamgee"))
|
||||
|
||||
val frodoJson: String =
|
||||
s"""{
|
||||
|"name": "${frodoUser.userName}"
|
||||
|}
|
||||
""".stripMargin
|
||||
|
||||
val hobbitGroupId = "uuid-12345-abcdef"
|
||||
val hobbitGroup: JsValue = Json.parse(s"""{
|
||||
| "id": "${hobbitGroupId}",
|
||||
| "name": "hobbits",
|
||||
| "email": "hobbitAdmin@shire.me",
|
||||
| "description": "Hobbits of the shire",
|
||||
| "members": [ { "id": "${frodoUser.id}" }, { "id": "samwise-userId" } ],
|
||||
| "admins": [ { "id": "${frodoUser.id}" } ]
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val ringbearerGroup: JsValue = Json.parse(
|
||||
s"""{
|
||||
| "id": "ringbearer-group-uuid",
|
||||
| "name": "ringbearers",
|
||||
| "email": "future-minions@mordor.me",
|
||||
| "description": "Corruptable folk of middle-earth",
|
||||
| "members": [ { "id": "${frodoUser.id}" }, { "id": "sauron-userId" } ],
|
||||
| "admins": [ { "id": "sauron-userId" } ]
|
||||
| }
|
||||
""".stripMargin
|
||||
)
|
||||
val hobbitGroupRequest: JsValue = Json.parse(s"""{
|
||||
| "name": "hobbits",
|
||||
| "email": "hobbitAdmin@shire.me",
|
||||
| "description": "Hobbits of the shire",
|
||||
| "members": [ { "id": "${frodoUser.id}" }, { "id": "samwise-userId" } ],
|
||||
| "admins": [ { "id": "${frodoUser.id}" } ]
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val invalidHobbitGroup: JsValue = Json.parse(s"""{
|
||||
| "name": "hobbits",
|
||||
| "email": "hobbitAdmin@shire.me",
|
||||
| "description": "Hobbits of the shire",
|
||||
| "members": [ { "id": "${frodoUser.id}" }, { "id": "merlin-userId" } ],
|
||||
| "admins": [ { "id": "${frodoUser.id}" } ]
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val hobbitGroupMembers: JsValue = Json.parse(
|
||||
s"""{
|
||||
| "members": [ ${frodoJsonString} ],
|
||||
| "maxItems": 100
|
||||
|}
|
||||
""".stripMargin
|
||||
)
|
||||
|
||||
val groupList: JsObject = Json.obj("groups" -> Json.arr(hobbitGroup))
|
||||
val emptyGroupList: JsObject = Json.obj("groups" -> Json.arr())
|
||||
|
||||
val frodoGroupList: JsObject = Json.obj("groups" -> Json.arr(hobbitGroup, ringbearerGroup))
|
||||
|
||||
val simulatedBackendPort: Int = 9001
|
||||
|
||||
val testConfig: Configuration =
|
||||
Configuration.load(Environment.simple()) ++ Configuration.from(
|
||||
Map("portal.vinyldns.backend.url" -> s"http://localhost:$simulatedBackendPort"))
|
||||
|
||||
val mockAuth: Authenticator = mock[Authenticator]
|
||||
val mockUserRepo: UserRepository = mock[UserRepository]
|
||||
val mockUserChangeRepo: UserChangeRepository = mock[UserChangeRepository]
|
||||
|
||||
mockAuth.authenticate("frodo", "secondbreakfast").returns(Success(frodoDetails))
|
||||
mockUserRepo.getUser(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
mockUserChangeRepo.save(any[UserChange]).returns(IO.pure(newFrodoLog))
|
||||
|
||||
def app: Application =
|
||||
GuiceApplicationBuilder()
|
||||
.disable[modules.VinylDNSModule]
|
||||
.bindings(
|
||||
bind[Authenticator].to(mockAuth),
|
||||
bind[UserRepository].to(mockUserRepo),
|
||||
bind[UserChangeRepository].to(mockUserChangeRepo)
|
||||
)
|
||||
.configure(testConfig)
|
||||
.build()
|
||||
}
|
@ -16,157 +16,78 @@
|
||||
|
||||
package controllers
|
||||
|
||||
import models.UserAccount
|
||||
import cats.effect.IO
|
||||
import org.joda.time.DateTime
|
||||
import org.specs2.mock.Mockito
|
||||
import org.specs2.mutable.Specification
|
||||
import org.specs2.specification.BeforeEach
|
||||
import vinyldns.core.domain.membership._
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
class UserAccountAccessorSpec extends Specification with Mockito with BeforeEach {
|
||||
|
||||
private val user = User(
|
||||
"fbaggins",
|
||||
"key",
|
||||
"secret",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now,
|
||||
"frodo-uuid")
|
||||
|
||||
private val userLog = UserChange(
|
||||
"frodo-uuid",
|
||||
user,
|
||||
"fbaggins",
|
||||
DateTime.now,
|
||||
None,
|
||||
UserChangeType.Create
|
||||
).toOption.get
|
||||
|
||||
private val mockRepo = mock[UserRepository]
|
||||
private val mockChangeRepo = mock[UserChangeRepository]
|
||||
private val underTest = new UserAccountAccessor(mockRepo, mockChangeRepo)
|
||||
|
||||
protected def before: Any =
|
||||
org.mockito.Mockito.reset(mockRepo, mockChangeRepo)
|
||||
|
||||
class UserAccountAccessorSpec extends Specification with Mockito {
|
||||
"User Account Accessor" should {
|
||||
"Return the user when storing a user that does not exist already" in {
|
||||
val mockStore = mock[UserAccountStore]
|
||||
val user = new UserAccount(
|
||||
"uuid",
|
||||
"fbaggins",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now(),
|
||||
"key",
|
||||
"secret")
|
||||
mockStore.storeUser(any[UserAccount]).returns(Success(user))
|
||||
|
||||
val underTest = new UserAccountAccessor(mockStore)
|
||||
underTest.put(user) must beASuccessfulTry(user)
|
||||
mockRepo.save(any[User]).returns(IO.pure(user))
|
||||
mockChangeRepo.save(any[UserChange]).returns(IO.pure(userLog))
|
||||
underTest.create(user).unsafeRunSync() must beEqualTo(user)
|
||||
there.was(one(mockChangeRepo).save(any[UserChange]))
|
||||
}
|
||||
|
||||
"Return the new user when storing a user that already exists in the store" in {
|
||||
val mockStore = mock[UserAccountStore]
|
||||
val oldUser = new UserAccount(
|
||||
"uuid",
|
||||
"fbaggins",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now(),
|
||||
"key",
|
||||
"secret")
|
||||
val newUser = new UserAccount(
|
||||
"uuid",
|
||||
"fbaggins",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now(),
|
||||
"new-key",
|
||||
"new-secret")
|
||||
mockStore.storeUser(any[UserAccount]).returns(Success(newUser))
|
||||
|
||||
val underTest = new UserAccountAccessor(mockStore)
|
||||
underTest.put(newUser) must beASuccessfulTry(newUser)
|
||||
}
|
||||
|
||||
"Return the failure when something goes wrong while storing a user" in {
|
||||
val mockStore = mock[UserAccountStore]
|
||||
val ex = new IllegalArgumentException("foobar")
|
||||
mockStore.storeUser(any[UserAccount]).returns(Failure(ex))
|
||||
val user = new UserAccount(
|
||||
"uuid",
|
||||
"fbaggins",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now(),
|
||||
"key",
|
||||
"secret")
|
||||
|
||||
val underTest = new UserAccountAccessor(mockStore)
|
||||
underTest.put(user) must beAFailedTry(ex)
|
||||
val newUser = user.copy(accessKey = "new-key", secretKey = "new-secret")
|
||||
mockRepo.save(any[User]).returns(IO.pure(newUser))
|
||||
mockChangeRepo.save(any[UserChange]).returns(IO.pure(userLog))
|
||||
underTest.update(newUser, user).unsafeRunSync() must beEqualTo(newUser)
|
||||
there.was(one(mockChangeRepo).save(any[UserChange]))
|
||||
}
|
||||
|
||||
"Return the user when retrieving a user that exists by name" in {
|
||||
val mockStore = mock[UserAccountStore]
|
||||
val user = new UserAccount(
|
||||
"uuid",
|
||||
"fbaggins",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now(),
|
||||
"key",
|
||||
"secret")
|
||||
mockStore.getUserByName(user.username).returns(Success(Some(user)))
|
||||
mockStore.getUserById(user.username).returns(Success(None))
|
||||
|
||||
val underTest = new UserAccountAccessor(mockStore)
|
||||
underTest.get("fbaggins") must beASuccessfulTry[Option[UserAccount]](Some(user))
|
||||
mockRepo.getUserByName(user.userName).returns(IO.pure(Some(user)))
|
||||
mockRepo.getUser(user.userName).returns(IO.pure(None))
|
||||
underTest.get("fbaggins").unsafeRunSync() must beSome(user)
|
||||
}
|
||||
|
||||
"Return the user when retrieving a user that exists by user id" in {
|
||||
val mockStore = mock[UserAccountStore]
|
||||
val user = new UserAccount(
|
||||
"uuid",
|
||||
"fbaggins",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now(),
|
||||
"key",
|
||||
"secret")
|
||||
mockStore.getUserByName(user.userId).returns(Success(None))
|
||||
mockStore.getUserById(user.userId).returns(Success(Some(user)))
|
||||
|
||||
val underTest = new UserAccountAccessor(mockStore)
|
||||
underTest.get("uuid") must beASuccessfulTry[Option[UserAccount]](Some(user))
|
||||
mockRepo.getUserByName(user.id).returns(IO.pure(None))
|
||||
mockRepo.getUser(user.id).returns(IO.pure(Some(user)))
|
||||
underTest.get(user.id).unsafeRunSync() must beSome(user)
|
||||
}
|
||||
|
||||
"Return None when the user to be retrieved does not exist" in {
|
||||
val mockStore = mock[UserAccountStore]
|
||||
mockStore.getUserByName(any[String]).returns(Success(None))
|
||||
mockStore.getUserById(any[String]).returns(Success(None))
|
||||
|
||||
val underTest = new UserAccountAccessor(mockStore)
|
||||
underTest.get("fbaggins") must beASuccessfulTry[Option[UserAccount]](None)
|
||||
mockRepo.getUserByName(any[String]).returns(IO.pure(None))
|
||||
mockRepo.getUser(any[String]).returns(IO.pure(None))
|
||||
underTest.get("fbaggins").unsafeRunSync() must beNone
|
||||
}
|
||||
|
||||
"Return the failure when the user cannot be looked up via user name" in {
|
||||
val mockStore = mock[UserAccountStore]
|
||||
val user = new UserAccount(
|
||||
"uuid",
|
||||
"fbaggins",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now(),
|
||||
"key",
|
||||
"secret")
|
||||
val ex = new IllegalArgumentException("foobar")
|
||||
mockStore.getUserByName(user.username).returns(Failure(ex))
|
||||
mockStore.getUserById(user.username).returns(Success(None))
|
||||
|
||||
val underTest = new UserAccountAccessor(mockStore)
|
||||
underTest.get("fbaggins") must beAFailedTry(ex)
|
||||
}
|
||||
|
||||
"Return the failure when the user cannot be looked up via user id" in {
|
||||
val mockStore = mock[UserAccountStore]
|
||||
val user = new UserAccount(
|
||||
"uuid",
|
||||
"fbaggins",
|
||||
Some("Frodo"),
|
||||
Some("Baggins"),
|
||||
Some("fbaggins@hobbitmail.me"),
|
||||
DateTime.now(),
|
||||
"key",
|
||||
"secret")
|
||||
val ex = new IllegalArgumentException("foobar")
|
||||
mockStore.getUserByName(user.userId).returns(Success(None))
|
||||
mockStore.getUserById(user.userId).returns(Failure(ex))
|
||||
|
||||
val underTest = new UserAccountAccessor(mockStore)
|
||||
underTest.get("uuid") must beAFailedTry(ex)
|
||||
"Return the user by access key" in {
|
||||
mockRepo.getUserByAccessKey(user.id).returns(IO.pure(Some(user)))
|
||||
underTest.getUserByKey(user.id).unsafeRunSync() must beSome(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers.datastores
|
||||
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient
|
||||
import controllers.{ChangeLogMessage, Create, UserChangeMessage}
|
||||
import models.UserAccount
|
||||
import org.joda.time.DateTime
|
||||
import org.specs2.mock.Mockito
|
||||
import org.specs2.mutable.Specification
|
||||
import play.api.{Configuration, Environment}
|
||||
import vinyldns.core.crypto.CryptoAlgebra
|
||||
|
||||
class DynamoDBChangeLogStoreSpec extends Specification with Mockito {
|
||||
private val testCrypto = new CryptoAlgebra {
|
||||
def encrypt(value: String): String = "encrypted!"
|
||||
def decrypt(value: String): String = "decrypted!"
|
||||
}
|
||||
|
||||
private val testUserAcc = UserAccount("foo", Some("bar"), Some("baz"), Some("qux"))
|
||||
private val testMessage =
|
||||
UserChangeMessage("foo", "bar", DateTime.now, Create, testUserAcc, Some(testUserAcc))
|
||||
"DynamoDbChangeLogStore" should {
|
||||
"accept a message and return it upon success" in {
|
||||
val (client, config) = buildMocks()
|
||||
val underTest = new DynamoDBChangeLogStore(client, config, testCrypto)
|
||||
val result = underTest.log(testMessage)
|
||||
result must beSuccessfulTry[ChangeLogMessage](testMessage)
|
||||
}
|
||||
|
||||
"accept a message and return it upon success when email, last name, and first name are none" in {
|
||||
val (client, config) = buildMocks()
|
||||
val underTest = new DynamoDBChangeLogStore(client, config, testCrypto)
|
||||
val user = testUserAcc.copy(firstName = None, lastName = None, email = None)
|
||||
val message = testMessage.copy(previousUser = None)
|
||||
|
||||
val result = underTest.log(message)
|
||||
result must beSuccessfulTry[ChangeLogMessage](message)
|
||||
}
|
||||
}
|
||||
|
||||
def buildMocks(): (AmazonDynamoDBClient, Configuration) = {
|
||||
val client = mock[AmazonDynamoDBClient]
|
||||
val config = Configuration.load(Environment.simple())
|
||||
(client, config)
|
||||
}
|
||||
|
||||
def buildTestStore(
|
||||
client: AmazonDynamoDBClient = mock[AmazonDynamoDBClient],
|
||||
config: Configuration = mock[Configuration]): DynamoDBUserAccountStore =
|
||||
new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers.datastores
|
||||
|
||||
import java.util
|
||||
|
||||
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient
|
||||
import com.amazonaws.services.dynamodbv2.model._
|
||||
import models.UserAccount
|
||||
import org.specs2.mock.Mockito
|
||||
import org.specs2.mutable.Specification
|
||||
import play.api.{Configuration, Environment}
|
||||
import vinyldns.core.crypto.CryptoAlgebra
|
||||
|
||||
class DynamoDBUserAccountStoreSpec extends Specification with Mockito {
|
||||
|
||||
val testCrypto = new CryptoAlgebra {
|
||||
def encrypt(value: String): String = "encrypted!"
|
||||
def decrypt(value: String): String = "decrypted!"
|
||||
}
|
||||
|
||||
"DynamoDBUserAccountStore" should {
|
||||
"Store a new user when email, first name, and last name are None" in {
|
||||
val (client, config) = buildMocks()
|
||||
val user = UserAccount("fbaggins", None, None, None)
|
||||
val item = DynamoDBUserAccountStore.toItem(user, testCrypto)
|
||||
val mockResult = mock[PutItemResult]
|
||||
mockResult.getAttributes.returns(item)
|
||||
client.putItem(any[PutItemRequest]).returns(mockResult)
|
||||
|
||||
val underTest = new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
val result = underTest.storeUser(user)
|
||||
result must beASuccessfulTry
|
||||
compareUserAccounts(result.get, user)
|
||||
}
|
||||
|
||||
"Store a new user when everything is ok" in {
|
||||
val (client, config) = buildMocks()
|
||||
val user =
|
||||
UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fbaggins@hobbitmail.me"))
|
||||
val item = DynamoDBUserAccountStore.toItem(user, testCrypto)
|
||||
val mockResult = mock[PutItemResult]
|
||||
mockResult.getAttributes.returns(item)
|
||||
client.putItem(any[PutItemRequest]).returns(mockResult)
|
||||
|
||||
val underTest = new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
val result = underTest.storeUser(user)
|
||||
result must beASuccessfulTry
|
||||
compareUserAccounts(result.get, user)
|
||||
}
|
||||
|
||||
"Store a user over an existing user returning the new user" in {
|
||||
val (client, config) = buildMocks()
|
||||
val oldUser = UserAccount("old", Some("Old"), Some("User"), Some("oldman@mail.me"))
|
||||
val newUser = oldUser.copy(username = "new")
|
||||
val mockResult = mock[PutItemResult]
|
||||
mockResult.getAttributes.returns(DynamoDBUserAccountStore.toItem(newUser, testCrypto))
|
||||
client.putItem(any[PutItemRequest]).returns(mockResult)
|
||||
|
||||
val underTest = new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
underTest.storeUser(oldUser)
|
||||
val result = underTest.storeUser(newUser)
|
||||
result must beASuccessfulTry
|
||||
compareUserAccounts(result.get, newUser)
|
||||
}
|
||||
|
||||
"Retrieve a given user based on user-id" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val (client, config) = buildMocks()
|
||||
val getResult = mock[GetItemResult]
|
||||
val resultItem = DynamoDBUserAccountStore.toItem(user, testCrypto)
|
||||
getResult.getItem.returns(resultItem)
|
||||
client.getItem(any[GetItemRequest]).returns(getResult)
|
||||
|
||||
val underTest = new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
val result = underTest.getUserById(user.userId)
|
||||
result must beASuccessfulTry
|
||||
result.get must beSome
|
||||
compareUserAccounts(result.get.get, user)
|
||||
}
|
||||
|
||||
"Retrieve a given user based on username" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val (client, config) = buildMocks()
|
||||
val queryResult = mock[QueryResult]
|
||||
val resultList = new util.ArrayList[util.Map[String, AttributeValue]]()
|
||||
resultList.add(DynamoDBUserAccountStore.toItem(user, testCrypto))
|
||||
queryResult.getItems.returns(resultList)
|
||||
queryResult.getCount.returns(1)
|
||||
client.query(any[QueryRequest]).returns(queryResult)
|
||||
|
||||
val underTest = new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
val result = underTest.getUserByName(user.username)
|
||||
result must beASuccessfulTry
|
||||
result.get must beSome
|
||||
compareUserAccounts(result.get.get, user)
|
||||
}
|
||||
|
||||
"Return a successful none if the user is not found by id (empty item)" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val (client, config) = buildMocks()
|
||||
val getResult = mock[GetItemResult]
|
||||
val resultItem = new util.HashMap[String, AttributeValue]()
|
||||
getResult.getItem.returns(resultItem)
|
||||
client.getItem(any[GetItemRequest]).returns(getResult)
|
||||
|
||||
val underTest = new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
val result = underTest.getUserById(user.userId)
|
||||
result must beASuccessfulTry[Option[UserAccount]](None)
|
||||
}
|
||||
|
||||
"Return a successful none if the user is not found by id (null)" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val (client, config) = buildMocks()
|
||||
val getResult = null
|
||||
client.getItem(any[GetItemRequest]).returns(getResult)
|
||||
|
||||
val underTest = new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
val result = underTest.getUserById(user.userId)
|
||||
result must beASuccessfulTry[Option[UserAccount]](None)
|
||||
}
|
||||
|
||||
"Return a successful none if the user is not found by name" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val (client, config) = buildMocks()
|
||||
val queryResult = mock[QueryResult]
|
||||
val resultList = new util.ArrayList[util.Map[String, AttributeValue]]()
|
||||
queryResult.getItems.returns(resultList)
|
||||
queryResult.getCount.returns(0)
|
||||
client.query(any[QueryRequest]).returns(queryResult)
|
||||
|
||||
val underTest = new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
val result = underTest.getUserByName(user.username)
|
||||
result must beASuccessfulTry(None)
|
||||
}
|
||||
|
||||
"Return a user based on username when more than one is found" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val secondUser =
|
||||
UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val thirdUser =
|
||||
UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val (client, config) = buildMocks()
|
||||
val queryResult = mock[QueryResult]
|
||||
val resultList = new util.ArrayList[util.Map[String, AttributeValue]]()
|
||||
resultList.add(DynamoDBUserAccountStore.toItem(user, testCrypto))
|
||||
resultList.add(DynamoDBUserAccountStore.toItem(secondUser, testCrypto))
|
||||
resultList.add(DynamoDBUserAccountStore.toItem(thirdUser, testCrypto))
|
||||
queryResult.getItems.returns(resultList)
|
||||
queryResult.getCount.returns(3)
|
||||
client.query(any[QueryRequest]).returns(queryResult)
|
||||
|
||||
val underTest = new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
val result = underTest.getUserByName(user.username)
|
||||
result must beASuccessfulTry
|
||||
result.get must beSome
|
||||
compareUserAccounts(result.get.get, user)
|
||||
}
|
||||
|
||||
"Encrypt the user secret" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val mockCrypto = mock[CryptoAlgebra]
|
||||
mockCrypto.encrypt(user.accessSecret).returns("hello")
|
||||
|
||||
val item = DynamoDBUserAccountStore.toItem(user, mockCrypto)
|
||||
item.get("secretkey").getS must beEqualTo("hello")
|
||||
|
||||
there.was(one(mockCrypto).encrypt(user.accessSecret))
|
||||
}
|
||||
|
||||
"Decrypt the user secret" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val mockCrypto = mock[CryptoAlgebra]
|
||||
mockCrypto.encrypt(user.accessSecret).returns("encrypt")
|
||||
mockCrypto.decrypt("encrypt").returns("decrypt")
|
||||
|
||||
val item = DynamoDBUserAccountStore.toItem(user, mockCrypto)
|
||||
val u = DynamoDBUserAccountStore.fromItem(item, mockCrypto)
|
||||
u.accessSecret must beEqualTo("decrypt")
|
||||
|
||||
there.was(one(mockCrypto).decrypt(item.get("secretkey").getS))
|
||||
}
|
||||
}
|
||||
|
||||
def buildMocks(): (AmazonDynamoDBClient, Configuration) = {
|
||||
val client = mock[AmazonDynamoDBClient]
|
||||
val config = Configuration.load(Environment.simple())
|
||||
(client, config)
|
||||
}
|
||||
|
||||
def buildTestStore(
|
||||
client: AmazonDynamoDBClient = mock[AmazonDynamoDBClient],
|
||||
config: Configuration = mock[Configuration]): DynamoDBUserAccountStore =
|
||||
new DynamoDBUserAccountStore(client, config, testCrypto)
|
||||
|
||||
def compareUserAccounts(actual: UserAccount, expected: UserAccount) = {
|
||||
actual.userId must beEqualTo(expected.userId)
|
||||
actual.created.compareTo(expected.created) must beEqualTo(0)
|
||||
actual.username must beEqualTo(expected.username)
|
||||
actual.firstName must beEqualTo(expected.firstName)
|
||||
actual.lastName must beEqualTo(expected.lastName)
|
||||
actual.accessKey must beEqualTo(expected.accessKey)
|
||||
testCrypto.decrypt(actual.accessSecret) must beEqualTo(
|
||||
testCrypto.decrypt(expected.accessSecret))
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers.datastores
|
||||
|
||||
import controllers.{ChangeLogMessage, Create, UserChangeMessage}
|
||||
import models.UserAccount
|
||||
import org.joda.time.DateTime
|
||||
import org.specs2.mock.Mockito
|
||||
import org.specs2.mutable.Specification
|
||||
|
||||
class InMemoryChangeLogStoreSpec extends Specification with Mockito {
|
||||
"InMemoryChangeLogStore" should {
|
||||
"accept a message and return it upon success" in {
|
||||
val underTest = new InMemoryChangeLogStore
|
||||
val userAcc = UserAccount("foo", "bar", None, None, None, DateTime.now, "ak", "sk")
|
||||
val message = UserChangeMessage("foo", "bar", DateTime.now, Create, userAcc, None)
|
||||
|
||||
val result = underTest.log(message)
|
||||
result must beASuccessfulTry[ChangeLogMessage](message)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers.datastores
|
||||
|
||||
import models.UserAccount
|
||||
import org.specs2.mock.Mockito
|
||||
import org.specs2.mutable.Specification
|
||||
|
||||
class InMemoryUserAccountStoreSpec extends Specification with Mockito {
|
||||
"InMemoryUserAccountStore" should {
|
||||
"Store a new user when everything is ok" in {
|
||||
val underTest = new InMemoryUserAccountStore()
|
||||
val user = mock[UserAccount]
|
||||
|
||||
val result = underTest.storeUser(user)
|
||||
result must beASuccessfulTry(user)
|
||||
}
|
||||
|
||||
"Store a user over an existing user returning the new user" in {
|
||||
val underTest = new InMemoryUserAccountStore()
|
||||
val oldUser = mock[UserAccount]
|
||||
oldUser.userId.returns("user")
|
||||
oldUser.username.returns("old")
|
||||
val newUser = mock[UserAccount]
|
||||
newUser.userId.returns("user")
|
||||
newUser.username.returns("new")
|
||||
|
||||
underTest.storeUser(oldUser)
|
||||
val result = underTest.storeUser(newUser)
|
||||
result must beASuccessfulTry(newUser)
|
||||
result.get.username must beEqualTo("new")
|
||||
}
|
||||
|
||||
"Retrieve a given user based on user-id" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val underTest = new InMemoryUserAccountStore()
|
||||
underTest.storeUser(user)
|
||||
|
||||
val result = underTest.getUserById(user.userId)
|
||||
result must beASuccessfulTry[Option[UserAccount]](Some(user))
|
||||
}
|
||||
|
||||
"Retrieve a given user based on username" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val underTest = new InMemoryUserAccountStore()
|
||||
underTest.storeUser(user)
|
||||
|
||||
val result = underTest.getUserByName(user.username)
|
||||
result must beASuccessfulTry[Option[UserAccount]](Some(user))
|
||||
}
|
||||
|
||||
"Return a successful none if the user is not found by id" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val underTest = new InMemoryUserAccountStore()
|
||||
|
||||
val result = underTest.getUserById(user.userId)
|
||||
result must beASuccessfulTry[Option[UserAccount]](None)
|
||||
}
|
||||
|
||||
"Return a successful none if the user is not found by name" in {
|
||||
val user = UserAccount("fbaggins", Some("Frodo"), Some("Baggins"), Some("fb@hobbitmail.me"))
|
||||
val underTest = new InMemoryUserAccountStore()
|
||||
|
||||
val result = underTest.getUserByName(user.username)
|
||||
result must beASuccessfulTry[Option[UserAccount]](None)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package models
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import org.specs2.mock.Mockito
|
||||
import org.specs2.mutable.Specification
|
||||
|
||||
class UserAccountSpec extends Specification with Mockito {
|
||||
"UserAccount" should {
|
||||
"Create a UserAccount from username, first name, last name and email" in {
|
||||
val username = "fbaggins"
|
||||
val fname = Some("Frodo")
|
||||
val lname = Some("Baggins")
|
||||
val email = Some("fb@hobbitmail.me")
|
||||
|
||||
val result = UserAccount(username, fname, lname, email)
|
||||
|
||||
result must beAnInstanceOf[UserAccount]
|
||||
UUID.fromString(result.userId) must beAnInstanceOf[UUID]
|
||||
result.username must beEqualTo(username)
|
||||
result.firstName must beEqualTo(fname)
|
||||
result.lastName must beEqualTo(lname)
|
||||
result.email must beEqualTo(email)
|
||||
result.accessKey.length must beEqualTo(20)
|
||||
result.accessSecret.length must beEqualTo(20)
|
||||
}
|
||||
|
||||
"Copy an existing UserAccount with different accessKey and accessSecret" in {
|
||||
val username = "fbaggins"
|
||||
val fname = Some("Frodo")
|
||||
val lname = Some("Baggins")
|
||||
val email = Some("fb@hobbitmail.me")
|
||||
|
||||
val result = UserAccount(username, fname, lname, email)
|
||||
val newResult = result.regenerateCredentials()
|
||||
|
||||
newResult must beAnInstanceOf[UserAccount]
|
||||
UUID.fromString(newResult.userId) must beEqualTo(UUID.fromString(result.userId))
|
||||
newResult.username must beEqualTo(username)
|
||||
newResult.firstName must beEqualTo(fname)
|
||||
newResult.lastName must beEqualTo(lname)
|
||||
newResult.email must beEqualTo(email)
|
||||
newResult.accessKey.length must beEqualTo(20)
|
||||
newResult.accessSecret.length must beEqualTo(20)
|
||||
newResult.accessKey mustNotEqual result.accessKey
|
||||
newResult.accessSecret mustNotEqual result.accessSecret
|
||||
}
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ object Dependencies {
|
||||
"dnsjava" % "dnsjava" % "2.1.7",
|
||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.2.3",
|
||||
"org.apache.commons" % "commons-lang3" % "3.4",
|
||||
"org.apache.commons" % "commons-text" % "1.4",
|
||||
"org.flywaydb" % "flyway-core" % "5.1.4",
|
||||
"org.json4s" %% "json4s-ext" % "3.5.3",
|
||||
"org.json4s" %% "json4s-jackson" % "3.5.3",
|
||||
@ -55,7 +56,8 @@ object Dependencies {
|
||||
"com.typesafe" % "config" % configV,
|
||||
"joda-time" % "joda-time" % "2.8.1",
|
||||
"org.scodec" %% "scodec-bits" % scodecV,
|
||||
"nl.grons" %% "metrics-scala" % metricsScalaV
|
||||
"nl.grons" %% "metrics-scala" % metricsScalaV,
|
||||
"org.apache.commons" % "commons-text" % "1.4"
|
||||
)
|
||||
|
||||
lazy val dynamoDBDependencies = Seq(
|
||||
@ -84,6 +86,8 @@ object Dependencies {
|
||||
"com.typesafe.play" %% "play-jdbc" % playV,
|
||||
"com.typesafe.play" %% "play-guice" % playV,
|
||||
"com.typesafe.play" %% "play-ahc-ws" % playV,
|
||||
"com.typesafe.play" %% "play-specs2" % playV % "test"
|
||||
"com.typesafe.play" %% "play-specs2" % playV % "test",
|
||||
"com.github.pureconfig" %% "pureconfig" % pureConfigV,
|
||||
"com.github.pureconfig" %% "pureconfig-cats-effect" % pureConfigV
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user