Skip to content

Commit

Permalink
Adds posibility to disable or enable existing sts account (#33)
Browse files Browse the repository at this point in the history
* adds possibility to enable or disable user accounts in sts

* one more it test

* Update readme file

* get docker from wbaa

* review comments included
  • Loading branch information
arempter authored and kr7ysztof committed Jul 18, 2019
1 parent ad4014b commit f5f3083
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 55 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ you will need to create the tables as well. You can find the script to create th

## Test (mock version)

`docker run -p 12345:12345 nielsdenissen/rokku-sts:latest`
`docker run -p 12345:12345 wbaa/rokku-sts:latest`

to get the credential you need to provide a valid token in on of the places:
* header `Authorization Bearer valid`
Expand Down Expand Up @@ -101,6 +101,14 @@ returns:
```http://localhost:12345/isCredentialActive?accessKey=okAccessKey&sessionToken=okSessionToken```
returns status OK or Forbidden

NOTE: since EP is protected with token, you may need to add header with token to access isCredentialsActive endpoint

```
Default token that should match settings from test reference.conf file
-H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXJ2aWNlIjoicm9ra3UiLCJpc3MiOiJyb2trdSJ9.aCpyvC53lWdF_IOdZQp0fO8W4tH_LeK3vQcIvt5W1-0"
```

### aws cli

```text
Expand Down Expand Up @@ -143,6 +151,21 @@ User must also:
When accessing Rokku with aws cli or sdk, just export `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`
with NO `AWS_SESSION_TOKEN`


### Enable or disable user account

STS user account details are taken from Keycloak, but additionally one can mark user account as disabled in Rokku-STS
by running:
```
Enable:
curl -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -X PUT http://localhost:12345/admin/account/{USER_NAME}/enable
Disable:
curl -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -X PUT http://localhost:12345/admin/account/{USER_NAME}/disable
```

User needs to be in administrator groups (user groups are taken from Keycloak). Check settings of the value `STS_ADMIN_GROUPS` in application.conf and set groups accordingly.

### Production settings

If you plan to run rokku-sts in non-dev mode, make sure you at least set ENV value or edit application.conf
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ services:
- 8080:8080

mariadb:
image: wbaa/rokku-dev-mariadb:0.0.4
image: wbaa/rokku-dev-mariadb:0.0.5
environment:
- MYSQL_ROOT_PASSWORD=admin
ports:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.ing.wbaa.rokku.sts.service.db.dao

import akka.actor.ActorSystem
import com.ing.wbaa.rokku.sts.config.{MariaDBSettings, StsSettings}
import com.ing.wbaa.rokku.sts.data.{UserGroup, UserName}
import com.ing.wbaa.rokku.sts.data.{AccountStatus, NPA, UserGroup, UserName}
import com.ing.wbaa.rokku.sts.data.aws.{AwsAccessKey, AwsCredential}
import com.ing.wbaa.rokku.sts.service.TokenGeneration
import com.ing.wbaa.rokku.sts.service.db.MariaDb
Expand Down Expand Up @@ -30,23 +30,23 @@ class STSUserAndGroupDAOItTest extends AsyncWordSpec with STSUserAndGroupDAO wit
"are new in the db and have a unique accesskey" in {
val testObject = new TestObject
insertAwsCredentials(testObject.userName, testObject.cred, isNpa = false).map(r => assert(r))
getAwsCredential(testObject.userName).map(c => assert(c.contains(testObject.cred)))
getUserSecretKeyAndIsNPA(testObject.cred.accessKey).map(c => assert(c.contains((testObject.userName, testObject.cred.secretKey, false, Set.empty[UserGroup]))))
getAwsCredentialAndStatus(testObject.userName).map{ case (c, _) => assert(c.contains(testObject.cred)) }
getUserSecretWithExtInfo(testObject.cred.accessKey).map(c => assert(c.contains((testObject.userName, testObject.cred.secretKey, NPA(false), AccountStatus(true), Set.empty[UserGroup]))))
}

"user is already present in the db" in {
val testObject = new TestObject
val newCred = generateAwsCredential

insertAwsCredentials(testObject.userName, testObject.cred, isNpa = false).flatMap { inserted =>
getAwsCredential(testObject.userName).map { c =>
getAwsCredentialAndStatus(testObject.userName).map { case (c, _) =>
assert(c.contains(testObject.cred))
assert(inserted)
}
}

insertAwsCredentials(testObject.userName, newCred, isNpa = false).flatMap(inserted =>
getAwsCredential(testObject.userName).map { c =>
getAwsCredentialAndStatus(testObject.userName).map { case (c, _) =>
assert(c.contains(testObject.cred))
assert(!inserted)
}
Expand All @@ -57,15 +57,15 @@ class STSUserAndGroupDAOItTest extends AsyncWordSpec with STSUserAndGroupDAO wit
val testObject = new TestObject

insertAwsCredentials(testObject.userName, testObject.cred, isNpa = false).flatMap { inserted =>
getAwsCredential(testObject.userName).map { c =>
getAwsCredentialAndStatus(testObject.userName).map { case (c, _) =>
assert(c.contains(testObject.cred))
assert(inserted)
}
}

val anotherTestObject = new TestObject
insertAwsCredentials(anotherTestObject.userName, testObject.cred, isNpa = false).flatMap(inserted =>
getAwsCredential(anotherTestObject.userName).map { c =>
getAwsCredentialAndStatus(anotherTestObject.userName).map { case (c, _) =>
assert(c.isEmpty)
assert(!inserted)
}
Expand All @@ -77,16 +77,16 @@ class STSUserAndGroupDAOItTest extends AsyncWordSpec with STSUserAndGroupDAO wit
"exists" in {
val testObject = new TestObject
insertAwsCredentials(testObject.userName, testObject.cred, isNpa = false)
getUserSecretKeyAndIsNPA(testObject.cred.accessKey).map { o =>
getUserSecretWithExtInfo(testObject.cred.accessKey).map { o =>
assert(o.isDefined)
assert(o.get._1 == testObject.userName)
assert(o.get._2 == testObject.cred.secretKey)
assert(!o.get._3)
assert(!o.get._3.value)
}
}

"doesn't exist" in {
getUserSecretKeyAndIsNPA(AwsAccessKey("DOESNTEXIST")).map { o =>
getUserSecretWithExtInfo(AwsAccessKey("DOESNTEXIST")).map { o =>
assert(o.isEmpty)
}
}
Expand All @@ -96,15 +96,15 @@ class STSUserAndGroupDAOItTest extends AsyncWordSpec with STSUserAndGroupDAO wit
"exists" in {
val testObject = new TestObject
insertAwsCredentials(testObject.userName, testObject.cred, isNpa = false)
getAwsCredential(testObject.userName).map { o =>
getAwsCredentialAndStatus(testObject.userName).map { case (o, _) =>
assert(o.isDefined)
assert(o.get.accessKey == testObject.cred.accessKey)
assert(o.get.secretKey == testObject.cred.secretKey)
}
}

"doesn't exist" in {
getAwsCredential(UserName("DOESNTEXIST")).map { o =>
getAwsCredentialAndStatus(UserName("DOESNTEXIST")).map { case (o, _) =>
assert(o.isEmpty)
}
}
Expand Down Expand Up @@ -151,14 +151,32 @@ class STSUserAndGroupDAOItTest extends AsyncWordSpec with STSUserAndGroupDAO wit
val testObject = new TestObject
insertAwsCredentials(testObject.userName, testObject.cred, isNpa = false)
insertUserGroups(testObject.userName, testObject.userGroups)
getUserSecretKeyAndIsNPA(testObject.cred.accessKey)
.map(c => assert(c.contains((testObject.userName, testObject.cred.secretKey, false, testObject.userGroups))))
getUserSecretWithExtInfo(testObject.cred.accessKey)
.map(c => assert(c.contains((testObject.userName, testObject.cred.secretKey, NPA(false), AccountStatus(true), testObject.userGroups))))
insertUserGroups(testObject.userName, Set(testObject.userGroups.head))
getUserSecretKeyAndIsNPA(testObject.cred.accessKey)
.map(c => assert(c.contains((testObject.userName, testObject.cred.secretKey, false, Set(testObject.userGroups.head)))))
getUserSecretWithExtInfo(testObject.cred.accessKey)
.map(c => assert(c.contains((testObject.userName, testObject.cred.secretKey, NPA(false), AccountStatus(true), Set(testObject.userGroups.head)))))
insertUserGroups(testObject.userName, Set.empty[UserGroup])
getUserSecretKeyAndIsNPA(testObject.cred.accessKey)
.map(c => assert(c.contains((testObject.userName, testObject.cred.secretKey, false, Set.empty[UserGroup]))))
getUserSecretWithExtInfo(testObject.cred.accessKey)
.map(c => assert(c.contains((testObject.userName, testObject.cred.secretKey, NPA(false), AccountStatus(true), Set.empty[UserGroup]))))
}
}

"disable or enable user" that {
"exists in sts records" in {
val testObject = new TestObject
val newUser = testObject.userName
val newCred = testObject.cred
insertAwsCredentials(newUser, newCred, isNpa = false).map(r => assert(r))

setAccountStatus(newUser, false)
getAwsCredentialAndStatus(newUser).map { case (_, AccountStatus(isEnabled)) => assert(!isEnabled) }
getUserSecretWithExtInfo(newCred.accessKey).map(c => assert(c.contains((newUser, newCred.secretKey, NPA(false), AccountStatus(false), Set.empty[UserGroup]))))

setAccountStatus(newUser, true)
getAwsCredentialAndStatus(newUser).map { case (_, AccountStatus(isEnabled)) => assert(isEnabled) }
getUserSecretWithExtInfo(newCred.accessKey).map(c => assert(c.contains((newUser, newCred.secretKey, NPA(false), AccountStatus(true), Set.empty[UserGroup]))))

}
}

Expand Down
26 changes: 25 additions & 1 deletion src/main/scala/com/ing/wbaa/rokku/sts/api/AdminApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ trait AdminApi extends LazyLogging with Encryption {
protected[this] def stsSettings: StsSettings

val adminRoutes: Route = pathPrefix("admin") {
addNPA
addNPA ~ setAccountStatus
}

case class ResponseMessage(code: String, message: String, target: String)
Expand All @@ -32,6 +32,8 @@ trait AdminApi extends LazyLogging with Encryption {

protected[this] def insertAwsCredentials(username: UserName, awsCredential: AwsCredential, isNpa: Boolean): Future[Boolean]

protected[this] def setAccountStatus(username: UserName, enabled: Boolean): Future[Boolean]

def userInAdminGroups(userGroups: Set[UserGroup]): Boolean =
userGroups.exists(g => stsSettings.adminGroups.contains(g.value))

Expand Down Expand Up @@ -62,4 +64,26 @@ trait AdminApi extends LazyLogging with Encryption {
}
}

def setAccountStatus: Route =
put {
path("account" / Segment / ("enable" | "disable")) { uid =>
authorizeToken(verifyAuthenticationToken) { keycloakUserInfo =>
extractUri { uri =>
if (userInAdminGroups(keycloakUserInfo.userGroups)) {
val action = uri.path.toString.split("/").last match {
case "enable" => true
case "disable" => false
}
onComplete(setAccountStatus(UserName(uid), action)) {
case Success(_) => complete(ResponseMessage(s"Account action", s"User account $uid enabled: $action", "user account"))
case Failure(ex) => complete(ResponseMessage("Account disable failed", ex.getMessage, "user account"))
}
} else {
reject(AuthorizationFailedRejection)
}
}
}
}
}

}
6 changes: 4 additions & 2 deletions src/main/scala/com/ing/wbaa/rokku/sts/api/STSApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.typesafe.scalalogging.LazyLogging

import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.util.{ Failure, Success }

trait STSApi extends LazyLogging with TokenXML {

Expand Down Expand Up @@ -51,8 +52,9 @@ trait STSApi extends LazyLogging with TokenXML {
private def getSessionTokenHandler: Route = {
getSessionTokenInputs { durationSeconds =>
authorizeToken(verifyAuthenticationToken) { keycloakUserInfo =>
onSuccess(getAwsCredentialWithToken(keycloakUserInfo.userName, keycloakUserInfo.userGroups, durationSeconds)) { awsCredentialWithToken =>
complete(getSessionTokenResponseToXML(awsCredentialWithToken))
onComplete(getAwsCredentialWithToken(keycloakUserInfo.userName, keycloakUserInfo.userGroups, durationSeconds)) {
case Success(awsCredentialWithToken) => complete(getSessionTokenResponseToXML(awsCredentialWithToken))
case Failure(_) => complete(StatusCodes.BadRequest)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.ing.wbaa.rokku.sts.data

case class AccountStatus(isEnabled: Boolean)
3 changes: 3 additions & 0 deletions src/main/scala/com/ing/wbaa/rokku/sts/data/NPA.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.ing.wbaa.rokku.sts.data

case class NPA(value: Boolean)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.ing.wbaa.rokku.sts.service
import java.time.Instant

import com.ing.wbaa.rokku.sts.data.aws._
import com.ing.wbaa.rokku.sts.data.{ STSUserInfo, UserGroup, UserName }
import com.ing.wbaa.rokku.sts.data.{ AccountStatus, NPA, STSUserInfo, UserGroup, UserName }
import com.typesafe.scalalogging.LazyLogging

import scala.concurrent.duration.Duration
Expand All @@ -13,9 +13,9 @@ trait UserTokenDbService extends LazyLogging with TokenGeneration {

implicit protected[this] def executionContext: ExecutionContext

protected[this] def getAwsCredential(userName: UserName): Future[Option[AwsCredential]]
protected[this] def getAwsCredentialAndStatus(userName: UserName): Future[(Option[AwsCredential], AccountStatus)]

protected[this] def getUserSecretKeyAndIsNPA(awsAccessKey: AwsAccessKey): Future[Option[(UserName, AwsSecretKey, Boolean, Set[UserGroup])]]
protected[this] def getUserSecretWithExtInfo(awsAccessKey: AwsAccessKey): Future[Option[(UserName, AwsSecretKey, NPA, AccountStatus, Set[UserGroup])]]

protected[this] def insertAwsCredentials(username: UserName, awsCredential: AwsCredential, isNpa: Boolean): Future[Boolean]

Expand All @@ -37,9 +37,10 @@ trait UserTokenDbService extends LazyLogging with TokenGeneration {
*/
def getAwsCredentialWithToken(userName: UserName, userGroups: Set[UserGroup], duration: Option[Duration]): Future[AwsCredentialWithToken] =
for {
awsCredential <- getOrGenerateAwsCredential(userName)
(awsCredential, AccountStatus(isEnabled)) <- getOrGenerateAwsCredentialWithStatus(userName)
awsSession <- getNewAwsSession(userName, duration)
_ <- insertUserGroups(userName, userGroups)
if isEnabled
} yield AwsCredentialWithToken(
awsCredential,
awsSession
Expand All @@ -51,18 +52,23 @@ trait UserTokenDbService extends LazyLogging with TokenGeneration {
* When a session token is not provided; this user has to be an NPA to be allowed access
*/
def isCredentialActive(awsAccessKey: AwsAccessKey, awsSessionToken: Option[AwsSessionToken]): Future[Option[STSUserInfo]] =
getUserSecretKeyAndIsNPA(awsAccessKey) flatMap {
case Some((userName, awsSecretKey, isNPA, groups)) =>
getUserSecretWithExtInfo(awsAccessKey) flatMap {
case Some((userName, awsSecretKey, NPA(isNPA), AccountStatus(isEnabled), groups)) =>
awsSessionToken match {
case Some(sessionToken) =>
case Some(sessionToken) if isEnabled =>
isTokenActive(sessionToken, userName).flatMap {
case true =>
getToken(sessionToken, userName)
.map(_ => Some(STSUserInfo(userName, groups, awsAccessKey, awsSecretKey)))
case false => Future.successful(None)
}

case None if isNPA =>
case Some(_) if !isEnabled =>
logger.warn(s"User validation failed. User account is not enabled in STS " +
s"(username: $userName, accessKey: $awsAccessKey)")
Future.successful(None)

case None if isNPA && isEnabled =>
Future.successful(Some(STSUserInfo(userName, Set.empty, awsAccessKey, awsSecretKey)))

case None if !isNPA =>
Expand Down Expand Up @@ -101,11 +107,14 @@ trait UserTokenDbService extends LazyLogging with TokenGeneration {
* Adds a user to the DB with aws credentials generated for it.
* In case the user already exists, it returns the already existing credentials.
*/
private[this] def getOrGenerateAwsCredential(userName: UserName): Future[AwsCredential] =
getAwsCredential(userName)
private[this] def getOrGenerateAwsCredentialWithStatus(userName: UserName): Future[(AwsCredential, AccountStatus)] =
getAwsCredentialAndStatus(userName)
.flatMap {
case Some(awsCredential) => Future.successful(awsCredential)
case None => getNewAwsCredential(userName)
case (Some(awsCredential), AccountStatus(isEnabled)) if isEnabled => Future.successful((awsCredential, AccountStatus(isEnabled)))
case (Some(awsCredential), AccountStatus(isEnabled)) if !isEnabled =>
logger.info(s"User account disabled for ${awsCredential.accessKey}")
Future.successful((awsCredential, AccountStatus(isEnabled)))
case (None, _) => getNewAwsCredential(userName).map(c => (c, AccountStatus(true)))
}

private[this] def getNewAwsCredential(userName: UserName): Future[AwsCredential] = {
Expand Down
Loading

0 comments on commit f5f3083

Please sign in to comment.