diff --git a/build.sbt b/build.sbt
index a623d6c..f5ee2a2 100644
--- a/build.sbt
+++ b/build.sbt
@@ -35,25 +35,25 @@ libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpVersion,
"com.typesafe.akka" %% "akka-http-xml" % akkaHttpVersion,
"com.typesafe.scala-logging" %% "scala-logging" % "3.9.2",
- "ch.qos.logback" % "logback-classic" % "1.4.1",
+ "ch.qos.logback" % "logback-classic" % "1.4.7",
"com.typesafe.akka" %% "akka-slf4j" % akkaVersion,
"org.keycloak" % "keycloak-core" % keycloakVersion,
"org.keycloak" % "keycloak-adapter-core" % keycloakVersion,
"org.keycloak" % "keycloak-admin-client" % keycloakVersion,
"org.jboss.logging" % "jboss-logging" % "3.5.0.Final",
- "org.apache.httpcomponents" % "httpclient" % "4.5.13",
+ "org.apache.httpcomponents" % "httpclient" % "4.5.14",
"ch.qos.logback.contrib" % "logback-json-classic" % logbackJson,
"ch.qos.logback.contrib" % "logback-jackson" % logbackJson,
- "com.auth0" % "java-jwt" % "4.0.0",
+ "com.auth0" % "java-jwt" % "4.3.0",
"com.bettercloud" % "vault-java-driver" % "5.1.0",
- "redis.clients" % "jedis" % "4.3.0-m1",
- "org.scalatest" %% "scalatest" % "3.2.13" % "test, it",
+ "redis.clients" % "jedis" % "4.4.0",
+ "org.scalatest" %% "scalatest" % "3.2.15" % "test, it",
"com.typesafe.akka" %% "akka-http-testkit" % akkaHttpVersion % Test,
"com.typesafe.akka" %% "akka-stream-testkit" % akkaVersion % Test,
- "com.amazonaws" % "aws-java-sdk-sts" % "1.12.307" % IntegrationTest,
+ "com.amazonaws" % "aws-java-sdk-sts" % "1.12.471" % IntegrationTest,
)
dependencyOverrides ++= Seq(
- "com.fasterxml.jackson.core" % "jackson-databind" % "2.14.2",
+ "com.fasterxml.jackson.core" % "jackson-databind" % "2.15.1",
)
configs(IntegrationTest)
diff --git a/src/it/scala/com/ing/wbaa/rokku/sts/StsServiceItTest.scala b/src/it/scala/com/ing/wbaa/rokku/sts/StsServiceItTest.scala
index 66a07d5..4241210 100644
--- a/src/it/scala/com/ing/wbaa/rokku/sts/StsServiceItTest.scala
+++ b/src/it/scala/com/ing/wbaa/rokku/sts/StsServiceItTest.scala
@@ -82,7 +82,7 @@ class StsServiceItTest extends AsyncWordSpec with Diagrams
override protected[this] def getToken(awsSessionToken: AwsSessionToken, username: Username): Future[Option[(Username, UserAssumeRole, AwsSessionTokenExpiration)]] =
Future.successful(None)
- override def generateAwsSession(duration: Option[Duration]): AwsSession = AwsSession(
+ override def generateAwsSession(duration: Duration): AwsSession = AwsSession(
AwsSessionToken("sessiontoken" + Random.alphanumeric.take(32).mkString),
AwsSessionTokenExpiration(Instant.now().plusSeconds(20))
)
diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf
index fa88f1b..1120541 100644
--- a/src/main/resources/application.conf
+++ b/src/main/resources/application.conf
@@ -37,6 +37,7 @@ rokku {
defaultTokenSessionHours = ${?STS_DEFAULT_TOKEN_SESSION_HOURS}
maxTokenSessionHours = ${?STS_MAX_TOKEN_SESSION_HOURS}
+ maxTokenSessionForNPAHours = ${?STS_MAX_TOKEN_SESSION_FOR_NPA_HOURS}
# at least 32 bytes long. Make sure you set your own random key
masterKey = ${?STS_MASTER_KEY}
encryptionAlgorithm = ${?STS_ENCRYPTION_ALGORITHM}
diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf
index 8804d3b..9999337 100644
--- a/src/main/resources/reference.conf
+++ b/src/main/resources/reference.conf
@@ -24,6 +24,7 @@ rokku {
defaultTokenSessionHours = 8
maxTokenSessionHours = 24
+ maxTokenSessionForNPAHours = 8760 #one year
masterKey = "MakeSureYouChangeMasterKeyToRandomString"
encryptionAlgorithm = "AES"
adminGroups = ""
diff --git a/src/main/scala/com/ing/wbaa/rokku/sts/api/STSApi.scala b/src/main/scala/com/ing/wbaa/rokku/sts/api/STSApi.scala
index a33584d..25fd66d 100644
--- a/src/main/scala/com/ing/wbaa/rokku/sts/api/STSApi.scala
+++ b/src/main/scala/com/ing/wbaa/rokku/sts/api/STSApi.scala
@@ -1,15 +1,15 @@
package com.ing.wbaa.rokku.sts.api
import java.util.concurrent.TimeUnit
-
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import com.ing.wbaa.rokku.sts.api.xml.TokenXML
+import com.ing.wbaa.rokku.sts.config.StsSettings
import com.ing.wbaa.rokku.sts.data._
import com.ing.wbaa.rokku.sts.data.aws._
import com.typesafe.scalalogging.LazyLogging
-import directive.STSDirectives.{ authorizeToken, assumeRole }
+import directive.STSDirectives.{ assumeRole, authorizeToken }
import scala.concurrent.Future
import scala.concurrent.duration._
@@ -17,29 +17,43 @@ import scala.util.{ Failure, Success }
trait STSApi extends LazyLogging with TokenXML {
+ protected[this] def stsSettings: StsSettings
+
private val getOrPost = get | post & pathSingleSlash
private val actionDirective = parameter("Action") | formField("Action")
- private val parseDurationSeconds: Option[Int] => Option[Duration] =
- _.map(durationSeconds => Duration(durationSeconds, TimeUnit.SECONDS))
+ private def parseDurationSeconds(aui: AuthenticationUserInfo, durationSeconds: Option[Int]): Duration = {
+ val maxTokenSession = if (aui.isNPA) stsSettings.maxTokenSessionForNPADuration else stsSettings.maxTokenSessionDuration
+ logger.debug("maxTokenSession {}", maxTokenSession)
+ val durationRequested = durationSeconds.map(ds => Duration(ds, TimeUnit.SECONDS))
+ val durationResult = durationRequested match {
+ case None => stsSettings.defaultTokenSessionDuration
+ case Some(durationRequested) =>
+ if (durationRequested > maxTokenSession) maxTokenSession
+ else durationRequested
+ }
+ logger.debug("durationResult {}", durationResult)
+ durationResult
+ }
- private val getSessionTokenInputs = {
+ private def getSessionTokenInputs(aui: AuthenticationUserInfo) = {
val input = "DurationSeconds".as[Int].?
(parameter(input) & formField(input)).tmap {
case (param, field) =>
- if (param.isDefined) parseDurationSeconds(param)
- else parseDurationSeconds(field)
+ if (param.isDefined) parseDurationSeconds(aui, param)
+ else parseDurationSeconds(aui, field)
}
}
- private val assumeRoleInputs = {
+ private def assumeRoleInputs(aui: AuthenticationUserInfo) = {
(parameters("RoleArn", "RoleSessionName", "DurationSeconds".as[Int].?) | formFields("RoleArn", "RoleSessionName", "DurationSeconds".as[Int].?)).tmap(t =>
- t.copy(_1 = AwsRoleArn(t._1), _3 = parseDurationSeconds(t._3))
+ t.copy(_1 = AwsRoleArn(t._1), _3 = parseDurationSeconds(aui, t._3))
)
}
- protected[this] def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], duration: Option[Duration]): Future[AwsCredentialWithToken]
- protected[this] def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], role: UserAssumeRole, duration: Option[Duration]): Future[AwsCredentialWithToken]
+ protected[this] def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], duration: Duration): Future[AwsCredentialWithToken]
+
+ protected[this] def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], role: UserAssumeRole, duration: Duration): Future[AwsCredentialWithToken]
// Keycloak
protected[this] def verifyAuthenticationToken(token: BearerToken): Option[AuthenticationUserInfo]
@@ -58,8 +72,8 @@ trait STSApi extends LazyLogging with TokenXML {
}
private def getSessionTokenHandler: Route = {
- getSessionTokenInputs { durationSeconds =>
- authorizeToken(verifyAuthenticationToken) { keycloakUserInfo =>
+ authorizeToken(verifyAuthenticationToken) { keycloakUserInfo =>
+ getSessionTokenInputs(keycloakUserInfo) { durationSeconds =>
onComplete(getAwsCredentialWithToken(keycloakUserInfo.userName, keycloakUserInfo.userGroups, durationSeconds)) {
case Success(awsCredentialWithToken) => complete(getSessionTokenResponseToXML(awsCredentialWithToken))
case Failure(ex) =>
@@ -72,8 +86,8 @@ trait STSApi extends LazyLogging with TokenXML {
}
private def assumeRoleHandler: Route = {
- assumeRoleInputs { (roleArn, roleSessionName, durationSeconds) =>
- authorizeToken(verifyAuthenticationToken) { keycloakUserInfo =>
+ authorizeToken(verifyAuthenticationToken) { keycloakUserInfo =>
+ assumeRoleInputs(keycloakUserInfo) { (roleArn, roleSessionName, durationSeconds) =>
assumeRole(keycloakUserInfo, roleArn) { assumeRole =>
onComplete(getAwsCredentialWithToken(keycloakUserInfo.userName, keycloakUserInfo.userGroups, assumeRole, durationSeconds)) {
case Success(awsCredentialWithToken) =>
diff --git a/src/main/scala/com/ing/wbaa/rokku/sts/config/StsSettings.scala b/src/main/scala/com/ing/wbaa/rokku/sts/config/StsSettings.scala
index 7876707..9be7a0f 100644
--- a/src/main/scala/com/ing/wbaa/rokku/sts/config/StsSettings.scala
+++ b/src/main/scala/com/ing/wbaa/rokku/sts/config/StsSettings.scala
@@ -11,6 +11,7 @@ class StsSettings(config: Config) extends Extension {
private[this] val rokkuStsConfig = config.getConfig("rokku.sts")
val defaultTokenSessionDuration: Duration = Duration(rokkuStsConfig.getInt("defaultTokenSessionHours"), TimeUnit.HOURS)
val maxTokenSessionDuration: Duration = Duration(rokkuStsConfig.getInt("maxTokenSessionHours"), TimeUnit.HOURS)
+ val maxTokenSessionForNPADuration: Duration = Duration(rokkuStsConfig.getInt("maxTokenSessionForNPAHours"), TimeUnit.HOURS)
val masterKey: String = rokkuStsConfig.getString("masterKey")
val encryptionAlgorithm: String = rokkuStsConfig.getString("encryptionAlgorithm")
val adminGroups = rokkuStsConfig.getString("adminGroups").split(",").map(_.trim).toList
diff --git a/src/main/scala/com/ing/wbaa/rokku/sts/data/AuthenticationUserInfo.scala b/src/main/scala/com/ing/wbaa/rokku/sts/data/AuthenticationUserInfo.scala
index 8add21a..6f455ec 100644
--- a/src/main/scala/com/ing/wbaa/rokku/sts/data/AuthenticationUserInfo.scala
+++ b/src/main/scala/com/ing/wbaa/rokku/sts/data/AuthenticationUserInfo.scala
@@ -8,4 +8,5 @@ case class AuthenticationUserInfo(
userName: Username,
userGroups: Set[UserGroup],
keycloakTokenId: AuthenticationTokenId,
- userRoles: Set[UserAssumeRole])
+ userRoles: Set[UserAssumeRole],
+ isNPA: Boolean)
diff --git a/src/main/scala/com/ing/wbaa/rokku/sts/keycloak/KeycloakTokenVerifier.scala b/src/main/scala/com/ing/wbaa/rokku/sts/keycloak/KeycloakTokenVerifier.scala
index 3c90714..26d9c84 100644
--- a/src/main/scala/com/ing/wbaa/rokku/sts/keycloak/KeycloakTokenVerifier.scala
+++ b/src/main/scala/com/ing/wbaa/rokku/sts/keycloak/KeycloakTokenVerifier.scala
@@ -22,6 +22,18 @@ trait KeycloakTokenVerifier extends LazyLogging {
import scala.jdk.CollectionConverters._
+ /**
+ * Temporary we define NPA by Name - later we will change it to some keycloak role
+ * @param keycloakToken
+ * @return true if NPA
+ */
+ private def isNPA(keycloakToken: AccessToken): Boolean = {
+ val isNPA = keycloakToken.getName == "NPA NPA"
+ logger.debug("user getName={}", keycloakToken.getName)
+ logger.debug("is NPA={}", isNPA)
+ isNPA
+ }
+
protected[this] def verifyAuthenticationToken(token: BearerToken): Option[AuthenticationUserInfo] = Try {
val accessToken = TokenVerifier.create(token.value, classOf[AccessToken])
@@ -39,7 +51,8 @@ trait KeycloakTokenVerifier extends LazyLogging {
.getOrDefault("user-groups", new util.ArrayList[String]())
.asInstanceOf[util.ArrayList[String]].asScala.toSet.map(UserGroup),
AuthenticationTokenId(keycloakToken.getId),
- keycloakToken.getRealmAccess.getRoles.asScala.toSet.map(UserAssumeRole)
+ keycloakToken.getRealmAccess.getRoles.asScala.toSet.map(UserAssumeRole),
+ isNPA(keycloakToken)
))
case Failure(exc: VerificationException) =>
logger.warn("Token (value={}) verification failed ex={}", token.value, exc.getMessage)
diff --git a/src/main/scala/com/ing/wbaa/rokku/sts/service/TokenGeneration.scala b/src/main/scala/com/ing/wbaa/rokku/sts/service/TokenGeneration.scala
index 93570b8..909cfdd 100644
--- a/src/main/scala/com/ing/wbaa/rokku/sts/service/TokenGeneration.scala
+++ b/src/main/scala/com/ing/wbaa/rokku/sts/service/TokenGeneration.scala
@@ -21,17 +21,10 @@ trait TokenGeneration {
AwsSecretKey(rand.alphanumeric.take(32).mkString)
)
- protected[this] def generateAwsSession(duration: Option[Duration]): AwsSession = {
- val tokenDuration = duration match {
- case None => stsSettings.defaultTokenSessionDuration
- case Some(durationRequested) =>
- if (durationRequested > stsSettings.maxTokenSessionDuration) stsSettings.maxTokenSessionDuration
- else durationRequested
- }
-
+ protected[this] def generateAwsSession(duration: Duration): AwsSession = {
AwsSession(
sessionToken = AwsSessionToken(rand.alphanumeric.take(32).mkString),
- expiration = AwsSessionTokenExpiration(Instant.now().plusMillis(tokenDuration.toMillis))
+ expiration = AwsSessionTokenExpiration(Instant.now().plusMillis(duration.toMillis))
)
}
}
diff --git a/src/main/scala/com/ing/wbaa/rokku/sts/service/UserTokenDbService.scala b/src/main/scala/com/ing/wbaa/rokku/sts/service/UserTokenDbService.scala
index 7ea9a73..72a0b45 100644
--- a/src/main/scala/com/ing/wbaa/rokku/sts/service/UserTokenDbService.scala
+++ b/src/main/scala/com/ing/wbaa/rokku/sts/service/UserTokenDbService.scala
@@ -43,7 +43,7 @@ trait UserTokenDbService extends LazyLogging with TokenGeneration {
* @param duration optional: the duration of the session, if duration is not given then it defaults to the application application default
* @return
*/
- def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], duration: Option[Duration]): Future[AwsCredentialWithToken] =
+ def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], duration: Duration): Future[AwsCredentialWithToken] =
for {
(awsCredential, AccountStatus(isEnabled)) <- getOrGenerateAwsCredentialWithStatus(userName)
awsSession <- getNewAwsSession(userName, duration)
@@ -63,7 +63,7 @@ trait UserTokenDbService extends LazyLogging with TokenGeneration {
* @param duration optional: the duration of the session, if duration is not given then it defaults to the application application default
* @return
*/
- def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], role: UserAssumeRole, duration: Option[Duration]): Future[AwsCredentialWithToken] =
+ def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], role: UserAssumeRole, duration: Duration): Future[AwsCredentialWithToken] =
for {
(awsCredential, AccountStatus(isEnabled)) <- getOrGenerateAwsCredentialWithStatus(userName)
awsSession <- getNewAwsSessionWithToken(userName, role, duration)
@@ -149,7 +149,7 @@ trait UserTokenDbService extends LazyLogging with TokenGeneration {
* @param generationTriesLeft Number of times to retry token generation, in case it collides
* @return
*/
- private[this] def getNewAwsSession(userName: Username, duration: Option[Duration], generationTriesLeft: Int = 3): Future[AwsSession] = {
+ private[this] def getNewAwsSession(userName: Username, duration: Duration, generationTriesLeft: Int = 3): Future[AwsSession] = {
val newAwsSession = generateAwsSession(duration)
insertToken(newAwsSession.sessionToken, userName, newAwsSession.expiration)
.flatMap {
@@ -172,7 +172,7 @@ trait UserTokenDbService extends LazyLogging with TokenGeneration {
* @param generationTriesLeft Number of times to retry token generation, in case it collides
* @return
*/
- private[this] def getNewAwsSessionWithToken(userName: Username, role: UserAssumeRole, duration: Option[Duration], generationTriesLeft: Int = 3): Future[AwsSession] = {
+ private[this] def getNewAwsSessionWithToken(userName: Username, role: UserAssumeRole, duration: Duration, generationTriesLeft: Int = 3): Future[AwsSession] = {
val newAwsSession = generateAwsSession(duration)
insertToken(newAwsSession.sessionToken, userName, role, newAwsSession.expiration)
.flatMap {
diff --git a/src/test/scala/com/ing/wbaa/rokku/sts/api/AdminApiTest.scala b/src/test/scala/com/ing/wbaa/rokku/sts/api/AdminApiTest.scala
index 7d09152..0dd97ed 100644
--- a/src/test/scala/com/ing/wbaa/rokku/sts/api/AdminApiTest.scala
+++ b/src/test/scala/com/ing/wbaa/rokku/sts/api/AdminApiTest.scala
@@ -32,8 +32,8 @@ class AdminApiTest extends AnyWordSpec
protected[this] def verifyAuthenticationToken(token: BearerToken): Option[AuthenticationUserInfo] =
token.value match {
- case "valid" => Some(AuthenticationUserInfo(Username("username"), Set(UserGroup("admins"), UserGroup("group2")), AuthenticationTokenId("tokenOk"), Set.empty))
- case "notAdmin" => Some(AuthenticationUserInfo(Username("username"), Set(UserGroup("group1"), UserGroup("group2")), AuthenticationTokenId("tokenOk"), Set.empty))
+ case "valid" => Some(AuthenticationUserInfo(Username("username"), Set(UserGroup("admins"), UserGroup("group2")), AuthenticationTokenId("tokenOk"), Set.empty, isNPA = false))
+ case "notAdmin" => Some(AuthenticationUserInfo(Username("username"), Set(UserGroup("group1"), UserGroup("group2")), AuthenticationTokenId("tokenOk"), Set.empty, isNPA = false))
case _ => None
}
diff --git a/src/test/scala/com/ing/wbaa/rokku/sts/api/NpaApiTest.scala b/src/test/scala/com/ing/wbaa/rokku/sts/api/NpaApiTest.scala
index 40f5022..972247c 100644
--- a/src/test/scala/com/ing/wbaa/rokku/sts/api/NpaApiTest.scala
+++ b/src/test/scala/com/ing/wbaa/rokku/sts/api/NpaApiTest.scala
@@ -63,9 +63,9 @@ class NpaApiTest extends AnyWordSpec
protected[this] def verifyAuthenticationToken(token: BearerToken): Option[AuthenticationUserInfo] =
token.value match {
- case "non-npa-user-token" => Some(AuthenticationUserInfo(Username(nonNpaUser), Set(), AuthenticationTokenId("nonNpaUser?"), Set()))
- case "npa-user-token" => Some(AuthenticationUserInfo(Username(npaUser), Set(), AuthenticationTokenId("npaUser"), Set(UserAssumeRole(keycloakSettings.npaRole))))
- case "npa-user-disabled-token" => Some(AuthenticationUserInfo(Username(disabledNpaUser), Set(), AuthenticationTokenId("disabledNpaUser"), Set(UserAssumeRole(keycloakSettings.npaRole))))
+ case "non-npa-user-token" => Some(AuthenticationUserInfo(Username(nonNpaUser), Set(), AuthenticationTokenId("nonNpaUser?"), Set(), isNPA = false))
+ case "npa-user-token" => Some(AuthenticationUserInfo(Username(npaUser), Set(), AuthenticationTokenId("npaUser"), Set(UserAssumeRole(keycloakSettings.npaRole)), isNPA = false))
+ case "npa-user-disabled-token" => Some(AuthenticationUserInfo(Username(disabledNpaUser), Set(), AuthenticationTokenId("disabledNpaUser"), Set(UserAssumeRole(keycloakSettings.npaRole)), isNPA = false))
case _ => None
}
diff --git a/src/test/scala/com/ing/wbaa/rokku/sts/api/STSApiTest.scala b/src/test/scala/com/ing/wbaa/rokku/sts/api/STSApiTest.scala
index c7c494d..6dfd9c5 100644
--- a/src/test/scala/com/ing/wbaa/rokku/sts/api/STSApiTest.scala
+++ b/src/test/scala/com/ing/wbaa/rokku/sts/api/STSApiTest.scala
@@ -1,17 +1,20 @@
package com.ing.wbaa.rokku.sts.api
-import java.time.Instant
+import akka.actor.ActorSystem
+import java.time.Instant
import akka.http.scaladsl.model.headers.Cookie
import akka.http.scaladsl.model.{ FormData, StatusCodes }
import akka.http.scaladsl.server.{ AuthorizationFailedRejection, MissingFormFieldRejection, MissingQueryParamRejection, Route }
import akka.http.scaladsl.testkit.ScalatestRouteTest
+import com.ing.wbaa.rokku.sts.config.StsSettings
import com.ing.wbaa.rokku.sts.data
import com.ing.wbaa.rokku.sts.data._
import com.ing.wbaa.rokku.sts.data.aws._
import org.scalatest.diagrams.Diagrams
import org.scalatest.wordspec.AnyWordSpec
+import java.util.concurrent.TimeUnit
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.xml.NodeSeq
@@ -32,11 +35,12 @@ class STSApiTest extends AnyWordSpec with Diagrams with ScalatestRouteTest {
override def verifyAuthenticationToken(token: BearerToken): Option[AuthenticationUserInfo] =
token.value match {
- case "valid" => Some(data.AuthenticationUserInfo(Username("name"), Set(UserGroup("testgroup")), AuthenticationTokenId("token"), Set(UserAssumeRole("testrole"))))
- case _ => None
+ case "valid" => Some(data.AuthenticationUserInfo(Username("name"), Set(UserGroup("testgroup")), AuthenticationTokenId("token"), Set(UserAssumeRole("testrole")), isNPA = false))
+ case "validNPA" => Some(data.AuthenticationUserInfo(Username("name"), Set(UserGroup("testgroup")), AuthenticationTokenId("token"), Set(UserAssumeRole("testrole")), isNPA = true))
+ case _ => None
}
- override protected[this] def getAwsCredentialWithToken(userName: Username, groups: Set[UserGroup], duration: Option[Duration]): Future[AwsCredentialWithToken] = {
+ override protected[this] def getAwsCredentialWithToken(userName: Username, groups: Set[UserGroup], duration: Duration): Future[AwsCredentialWithToken] = {
Future.successful(AwsCredentialWithToken(
AwsCredential(
AwsAccessKey("accesskey"),
@@ -44,12 +48,12 @@ class STSApiTest extends AnyWordSpec with Diagrams with ScalatestRouteTest {
),
AwsSession(
AwsSessionToken("token"),
- AwsSessionTokenExpiration(Instant.ofEpochMilli(duration.getOrElse(1.second).toMillis))
+ AwsSessionTokenExpiration(Instant.ofEpochMilli(duration.toMillis))
)
))
}
- override protected[this] def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], role: UserAssumeRole, duration: Option[Duration]): Future[AwsCredentialWithToken] = {
+ override protected[this] def getAwsCredentialWithToken(userName: Username, userGroups: Set[UserGroup], role: UserAssumeRole, duration: Duration): Future[AwsCredentialWithToken] = {
Future.successful(AwsCredentialWithToken(
AwsCredential(
AwsAccessKey("accesskey"),
@@ -57,10 +61,17 @@ class STSApiTest extends AnyWordSpec with Diagrams with ScalatestRouteTest {
),
AwsSession(
AwsSessionToken("token"),
- AwsSessionTokenExpiration(Instant.ofEpochMilli(duration.getOrElse(1.second).toMillis))
+ AwsSessionTokenExpiration(Instant.ofEpochMilli(duration.toMillis))
)
))
}
+
+ val testSystem: ActorSystem = ActorSystem.create("test-system")
+ override protected[this] def stsSettings: StsSettings = new StsSettings(testSystem.settings.config) {
+ override val defaultTokenSessionDuration: Duration = Duration(8, TimeUnit.HOURS)
+ override val maxTokenSessionDuration: Duration = Duration(24, TimeUnit.HOURS)
+ override val maxTokenSessionForNPADuration: Duration = Duration(10, TimeUnit.DAYS)
+ }
}
private val s3Routes: Route = new MockStsApi().stsRoutes
@@ -76,13 +87,16 @@ class STSApiTest extends AnyWordSpec with Diagrams with ScalatestRouteTest {
}.stsRoutes
val validOAuth2TokenHeader: RequestTransformer = addHeader("Authorization", "Bearer valid")
+ val validNPAOAuth2TokenHeader: RequestTransformer = addHeader("Authorization", "Bearer validNPA")
val validOAuth2TokenCookie: RequestTransformer = addHeader(Cookie("X-Authorization-Token", "valid"))
val invalidOAuth2TokenHeader: RequestTransformer = addHeader("Authorization", "Bearer invalid")
val invalidOAuth2TokenCookie: RequestTransformer = addHeader(Cookie("X-Authorization-Token", "invalid"))
val actionGetSessionToken = "?Action=GetSessionToken"
val actionAssumeRole = "?Action=AssumeRole"
- val durationQuery = "&DurationSeconds=3600"
+ val duration1hQuery = "&DurationSeconds=3600"
+ val duration48hQuery = "&DurationSeconds=172800"
+ val duration20dQuery = "&DurationSeconds=1728000"
val roleNameSessionQuery = "&RoleSessionName=app1"
val arnQuery = "&RoleArn=arn:aws:iam::123456789012:role/testrole"
val webIdentityTokenQuery = "&WebIdentityToken=Atza%7CIQ"
@@ -111,12 +125,33 @@ class STSApiTest extends AnyWordSpec with Diagrams with ScalatestRouteTest {
}
"return a session token with 1h expiration time because valid credentials" in {
- Get(s"/$actionGetSessionToken$durationQuery") ~> validOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
+ Get(s"/$actionGetSessionToken$duration1hQuery") ~> validOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
assert(status == StatusCodes.OK)
assert(responseAs[String] == "1970-01-01T01:00:00Z")
}
}
+ "return max session token with 24h expiration time because longer time is not allowed for users" in {
+ Get(s"/$actionGetSessionToken$duration48hQuery") ~> validOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
+ assert(status == StatusCodes.OK)
+ assert(responseAs[String] == "1970-01-02T00:00:00Z")
+ }
+ }
+
+ "return a session token with 48h expiration time because NPA can have longer time" in {
+ Get(s"/$actionGetSessionToken$duration48hQuery") ~> validNPAOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
+ assert(status == StatusCodes.OK)
+ assert(responseAs[String] == "1970-01-03T00:00:00Z")
+ }
+ }
+
+ "return max session token with 10 days expiration time because longer time is not allowed for NPA" in {
+ Get(s"/$actionGetSessionToken$duration20dQuery") ~> validNPAOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
+ assert(status == StatusCodes.OK)
+ assert(responseAs[String] == "1970-01-11T00:00:00Z")
+ }
+ }
+
"return rejection because invalid credentials" in {
Get(s"/$actionGetSessionToken") ~> invalidOAuth2TokenHeader ~> s3Routes ~> check {
assert(rejections.contains(AuthorizationFailedRejection))
@@ -153,7 +188,7 @@ class STSApiTest extends AnyWordSpec with Diagrams with ScalatestRouteTest {
}
"return a session token with 1h expiration time because valid credentials" in {
- Post("/", FormData(queryToFormData(actionGetSessionToken, durationQuery))) ~> validOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
+ Post("/", FormData(queryToFormData(actionGetSessionToken, duration1hQuery))) ~> validOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
assert(status == StatusCodes.OK)
assert(responseAs[String] == "1970-01-01T01:00:00Z")
}
@@ -170,7 +205,7 @@ class STSApiTest extends AnyWordSpec with Diagrams with ScalatestRouteTest {
}
"return a session token with 1h expiration time because credentials are valid" in {
- Get(s"/$actionAssumeRole$durationQuery$arnQuery$roleNameSessionQuery") ~> validOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
+ Get(s"/$actionAssumeRole$duration1hQuery$arnQuery$roleNameSessionQuery") ~> validOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
assert(status == StatusCodes.OK)
assert(responseAs[String] == "1970-01-01T01:00:00Z")
}
@@ -183,13 +218,13 @@ class STSApiTest extends AnyWordSpec with Diagrams with ScalatestRouteTest {
}
"return rejection because no arn parameter" in {
- Get(s"/$actionAssumeRole$roleNameSessionQuery") ~> invalidOAuth2TokenHeader ~> s3Routes ~> check {
+ Get(s"/$actionAssumeRole$roleNameSessionQuery") ~> validOAuth2TokenHeader ~> s3Routes ~> check {
assert(rejections.contains(MissingQueryParamRejection("RoleArn")))
}
}
"return rejection because no roleSessionName parameter" in {
- Get(s"/$actionAssumeRole$arnQuery") ~> invalidOAuth2TokenHeader ~> s3Routes ~> check {
+ Get(s"/$actionAssumeRole$arnQuery") ~> validOAuth2TokenHeader ~> s3Routes ~> check {
assert(rejections.contains(MissingQueryParamRejection("RoleSessionName")))
}
}
@@ -204,38 +239,38 @@ class STSApiTest extends AnyWordSpec with Diagrams with ScalatestRouteTest {
"STS api the POST method for assumeRole" should {
"return a session token because credentials are valid" in {
- Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery, roleNameSessionQuery, durationQuery))) ~> validOAuth2TokenHeader ~> s3Routes ~> check {
+ Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery, roleNameSessionQuery, duration1hQuery))) ~> validOAuth2TokenHeader ~> s3Routes ~> check {
assert(status == StatusCodes.OK)
assert(responseAs[String] == "")
}
}
"return rejection because invalid credentials" in {
- Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery, roleNameSessionQuery, durationQuery))) ~> invalidOAuth2TokenHeader ~> s3Routes ~> check {
+ Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery, roleNameSessionQuery, duration1hQuery))) ~> invalidOAuth2TokenHeader ~> s3Routes ~> check {
assert(rejections.contains(AuthorizationFailedRejection))
}
}
"return rejection because no arn parameter" in {
- Post("/", FormData(queryToFormData(actionAssumeRole, roleNameSessionQuery, durationQuery))) ~> invalidOAuth2TokenHeader ~> s3Routes ~> check {
+ Post("/", FormData(queryToFormData(actionAssumeRole, roleNameSessionQuery, duration1hQuery))) ~> validOAuth2TokenHeader ~> s3Routes ~> check {
assert(rejections.contains(MissingFormFieldRejection("RoleArn")))
}
}
"return rejection because no roleSessionName parameter" in {
- Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery))) ~> invalidOAuth2TokenHeader ~> s3Routes ~> check {
+ Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery))) ~> validOAuth2TokenHeader ~> s3Routes ~> check {
assert(rejections.contains(MissingFormFieldRejection("RoleSessionName")))
}
}
"return rejection because no credentials" in {
- Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery, roleNameSessionQuery, durationQuery))) ~> s3Routes ~> check {
+ Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery, roleNameSessionQuery, duration1hQuery))) ~> s3Routes ~> check {
assert(rejections.contains(AuthorizationFailedRejection))
}
}
"return a session token with 1h expiration time because valid credentials" in {
- Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery, roleNameSessionQuery, durationQuery))) ~> validOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
+ Post("/", FormData(queryToFormData(actionAssumeRole, arnQuery, roleNameSessionQuery, duration1hQuery))) ~> validOAuth2TokenHeader ~> s3RoutesWithExpirationTime ~> check {
assert(status == StatusCodes.OK)
assert(responseAs[String] == "1970-01-01T01:00:00Z")
}
diff --git a/src/test/scala/com/ing/wbaa/rokku/sts/data/aws/AwsRoleArnTest.scala b/src/test/scala/com/ing/wbaa/rokku/sts/data/aws/AwsRoleArnTest.scala
index 400ace4..41172c2 100644
--- a/src/test/scala/com/ing/wbaa/rokku/sts/data/aws/AwsRoleArnTest.scala
+++ b/src/test/scala/com/ing/wbaa/rokku/sts/data/aws/AwsRoleArnTest.scala
@@ -13,7 +13,7 @@ class AwsRoleArnTest extends AnyWordSpec {
val result = AwsRoleArn(s"arn:aws:iam::123456789012:role/$testRoleName")
.getRoleUserCanAssume(
- AuthenticationUserInfo(Username(""), Set.empty, AuthenticationTokenId(""), Set(UserAssumeRole(testRoleName)))
+ AuthenticationUserInfo(Username(""), Set.empty, AuthenticationTokenId(""), Set(UserAssumeRole(testRoleName)), isNPA = false)
)
assert(result.contains(UserAssumeRole(testRoleName)))
}
@@ -23,7 +23,7 @@ class AwsRoleArnTest extends AnyWordSpec {
val result = AwsRoleArn(s"arn:aws:iam:invalid:123456789012:role/$testRoleName")
.getRoleUserCanAssume(
- AuthenticationUserInfo(Username(""), Set.empty, AuthenticationTokenId(""), Set(UserAssumeRole(testRoleName)))
+ AuthenticationUserInfo(Username(""), Set.empty, AuthenticationTokenId(""), Set(UserAssumeRole(testRoleName)), isNPA = false)
)
assert(result.isEmpty)
}
@@ -33,7 +33,7 @@ class AwsRoleArnTest extends AnyWordSpec {
val result = AwsRoleArn(s"arn:aws:iam::123456789012:role/$testRoleName")
.getRoleUserCanAssume(
- AuthenticationUserInfo(Username(""), Set.empty, AuthenticationTokenId(""), Set.empty)
+ AuthenticationUserInfo(Username(""), Set.empty, AuthenticationTokenId(""), Set.empty, isNPA = false)
)
assert(result.isEmpty)
}
diff --git a/src/test/scala/com/ing/wbaa/rokku/sts/service/TokenGenerationTest.scala b/src/test/scala/com/ing/wbaa/rokku/sts/service/TokenGenerationTest.scala
index 6343746..33f27a1 100644
--- a/src/test/scala/com/ing/wbaa/rokku/sts/service/TokenGenerationTest.scala
+++ b/src/test/scala/com/ing/wbaa/rokku/sts/service/TokenGenerationTest.scala
@@ -14,10 +14,7 @@ import scala.concurrent.duration.Duration
class TokenGenerationTest extends AnyWordSpec with Diagrams with TokenGeneration {
val testSystem: ActorSystem = ActorSystem.create("test-system")
- override protected[this] def stsSettings: StsSettings = new StsSettings(testSystem.settings.config) {
- override val defaultTokenSessionDuration: Duration = Duration(8, TimeUnit.HOURS)
- override val maxTokenSessionDuration: Duration = Duration(24, TimeUnit.HOURS)
- }
+ override protected[this] def stsSettings: StsSettings = new StsSettings(testSystem.settings.config)
"Token generation" should {
val allowedCharacters = (('a' to 'z') ++ ('A' to 'Z') ++ ('0' to '9')).toSet
@@ -35,25 +32,12 @@ class TokenGenerationTest extends AnyWordSpec with Diagrams with TokenGeneration
assert(diff >= 0 && diff < allowedOffsetMillis)
}
- "has no duration specified" in {
- val generatedAwsSession = generateAwsSession(None)
- assert(generatedAwsSession.sessionToken.value.forall(allowedCharacters.contains))
- assertExpirationValid(generatedAwsSession.expiration, stsSettings.defaultTokenSessionDuration)
- }
-
- "has duration within range of max specified" in {
+ "has duration set to 2h" in {
val customDuration = Duration(2, TimeUnit.HOURS)
- val generatedAwsSession = generateAwsSession(Some(customDuration))
+ val generatedAwsSession = generateAwsSession(customDuration)
assert(generatedAwsSession.sessionToken.value.forall(allowedCharacters.contains))
assertExpirationValid(generatedAwsSession.expiration, customDuration)
}
-
- "has duration larger than max specified" in {
- val customDuration = Duration(25, TimeUnit.HOURS)
- val generatedAwsSession = generateAwsSession(Some(customDuration))
- assert(generatedAwsSession.sessionToken.value.forall(allowedCharacters.contains))
- assertExpirationValid(generatedAwsSession.expiration, stsSettings.maxTokenSessionDuration)
- }
}
}
diff --git a/src/test/scala/com/ing/wbaa/rokku/sts/service/UserTokenDbServiceTest.scala b/src/test/scala/com/ing/wbaa/rokku/sts/service/UserTokenDbServiceTest.scala
index b5d354e..5c10d98 100644
--- a/src/test/scala/com/ing/wbaa/rokku/sts/service/UserTokenDbServiceTest.scala
+++ b/src/test/scala/com/ing/wbaa/rokku/sts/service/UserTokenDbServiceTest.scala
@@ -67,7 +67,7 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
"are new credentials and a new token with specified duration" in {
val testObject = new TestObject
- new UserTokenDbServiceTest {}.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], Some(testObject.duration)).map { c =>
+ new UserTokenDbServiceTest {}.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], testObject.duration).map { c =>
assertExpirationValid(c.session.expiration, testObject.duration)
}
}
@@ -77,7 +77,7 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
new UserTokenDbServiceTest {
override protected[this] def getUserAccountByName(userName: Username): Future[Option[UserAccount]] =
Future.successful(Some(UserAccount(userName, None, AccountStatus(false), NPA(false), Set())))
- }.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], Some(testObject.duration)).map { c =>
+ }.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], testObject.duration).map { c =>
assertExpirationValid(c.session.expiration, testObject.duration)
}
}
@@ -94,7 +94,7 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
}
recoverToSucceededIf[Exception] {
- utdst.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], Some(testObject.duration)).map { c =>
+ utdst.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], testObject.duration).map { c =>
assertExpirationValid(c.session.expiration, testObject.duration)
}
}
@@ -104,8 +104,8 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
"have existing credentials and a new token" in {
val testObject = new TestObject
val utds = new UserTokenDbServiceTest {}
- utds.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], None).flatMap { firstReturn =>
- utds.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], None).map { secondReturn =>
+ utds.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], testObject.duration).flatMap { firstReturn =>
+ utds.getAwsCredentialWithToken(testObject.userName, Set.empty[UserGroup], testObject.duration).map { secondReturn =>
assert(firstReturn.awsCredential == secondReturn.awsCredential)
assert(firstReturn.session != secondReturn.session)
}
@@ -117,7 +117,7 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
"has valid accesskey and sessiontoken is active" in {
val t = new TestObject
val utds = new UserTokenDbServiceTest {}
- utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], Some(t.duration)).flatMap { awsCredWithToken =>
+ utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], t.duration).flatMap { awsCredWithToken =>
utds.isCredentialActive(awsCredWithToken.awsCredential.accessKey, Some(awsCredWithToken.session.sessionToken)).map { u =>
assert(u.map(_.userName).contains(Username("u")))
assert(u.map(_.awsAccessKey).contains(AwsAccessKey("a")))
@@ -134,7 +134,7 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
override protected[this] def getToken(awsSessionToken: AwsSessionToken, userName: Username): Future[Option[(Username, UserAssumeRole, AwsSessionTokenExpiration)]] =
Future.successful(Some((Username("u"), UserAssumeRole("testGroup"), AwsSessionTokenExpiration(Instant.now().minusSeconds(20)))))
}
- utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], Some(Duration(-1, TimeUnit.HOURS)))
+ utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], Duration(-1, TimeUnit.HOURS))
.flatMap { awsCredWithToken =>
utds.isCredentialActive(awsCredWithToken.awsCredential.accessKey, Some(awsCredWithToken.session.sessionToken))
.map(b => assert(b.isEmpty))
@@ -147,7 +147,7 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
override protected[this] def getToken(awsSessionToken: AwsSessionToken, userName: Username): Future[Option[(Username, UserAssumeRole, AwsSessionTokenExpiration)]] =
Future.successful(Some((Username("u"), UserAssumeRole("testRole"), AwsSessionTokenExpiration(Instant.now().plusSeconds(20)))))
}
- utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], Some(t.duration))
+ utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], t.duration)
.flatMap { awsCredWithToken =>
utds.isCredentialActive(awsCredWithToken.awsCredential.accessKey, Some(awsCredWithToken.session.sessionToken)).map { u =>
assert(u.map(_.userName).contains(Username("u")))
@@ -167,7 +167,7 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
"has valid accesskey, no sessiontoken and is not an NPA" in {
val t = new TestObject
val utds = new UserTokenDbServiceTest {}
- utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], Some(t.duration)).flatMap { awsCredWithToken =>
+ utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], t.duration).flatMap { awsCredWithToken =>
utds.isCredentialActive(awsCredWithToken.awsCredential.accessKey, None).map(a => assert(a.isEmpty))
}
}
@@ -178,7 +178,7 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
override protected[this] def getUserAccountByAccessKey(awsAccessKey: AwsAccessKey): Future[Option[UserAccount]] =
Future.successful(Some(UserAccount(Username("u"), Some(AwsCredential(AwsAccessKey("a"), AwsSecretKey("s"))), AccountStatus(true), NPA(true), Set.empty[UserGroup])))
}
- utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], Some(t.duration)).flatMap { awsCredWithToken =>
+ utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], t.duration).flatMap { awsCredWithToken =>
utds.isCredentialActive(awsCredWithToken.awsCredential.accessKey, None).map(a => assert(a.isDefined))
}
}
@@ -189,7 +189,7 @@ class UserTokenDbServiceTest extends AsyncWordSpec with Diagrams {
override protected[this] def getUserAccountByAccessKey(awsAccessKey: AwsAccessKey): Future[Option[UserAccount]] =
Future.successful(Some(UserAccount(Username("u"), Some(AwsCredential(AwsAccessKey("a"), AwsSecretKey("s"))), AccountStatus(false), NPA(false), Set.empty[UserGroup])))
}
- utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], Some(t.duration)).flatMap { awsCredWithToken =>
+ utds.getAwsCredentialWithToken(t.userName, Set.empty[UserGroup], t.duration).flatMap { awsCredWithToken =>
utds.isCredentialActive(awsCredWithToken.awsCredential.accessKey, Some(awsCredWithToken.session.sessionToken)).map(a => assert(!a.isDefined))
}
}