Skip to content

Commit

Permalink
Merge pull request #51 from ing-bank/feature/keycloak-user-endpoint
Browse files Browse the repository at this point in the history
add keycloak-user endpoint
  • Loading branch information
kr7ysztof committed Aug 19, 2020
2 parents 0e046e3 + b0ec583 commit 837ffe8
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 9 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ libraryDependencies ++= Seq(
"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.3.2.Final",
"org.apache.httpcomponents" % "httpclient" % "4.5.6",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.3.0",
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ version: "2"
services:

keycloak:
image: wbaa/rokku-dev-keycloak:0.0.6
image: wbaa/rokku-dev-keycloak:0.0.9
environment:
- KEYCLOAK_USER=admin
- KEYCLOAK_PASSWORD=admin
- DB_VENDOR=h2
ports:
- 8080:8080

Expand Down
5 changes: 3 additions & 2 deletions src/it/scala/com/ing/wbaa/rokku/sts/StsServiceItTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.ing.wbaa.rokku.sts.config.{HttpSettings, KeycloakSettings, MariaDBSet
import com.ing.wbaa.rokku.sts.data.aws._
import com.ing.wbaa.rokku.sts.data.{UserAssumeRole, UserName}
import com.ing.wbaa.rokku.sts.helper.{KeycloackToken, OAuth2TokenRequest}
import com.ing.wbaa.rokku.sts.keycloak.KeycloakTokenVerifier
import com.ing.wbaa.rokku.sts.keycloak.{ KeycloakClient, KeycloakTokenVerifier }
import com.ing.wbaa.rokku.sts.service.UserTokenDbService
import com.ing.wbaa.rokku.sts.service.db.MariaDb
import com.ing.wbaa.rokku.sts.service.db.dao.STSUserAndGroupDAO
Expand Down Expand Up @@ -53,7 +53,8 @@ class StsServiceItTest extends AsyncWordSpec with Diagrams
with UserTokenDbService
with STSUserAndGroupDAO
with MariaDb
with VaultService {
with VaultService
with KeycloakClient {
override implicit def system: ActorSystem = testSystem

override protected[this] def httpSettings: HttpSettings = rokkuHttpSettings
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.ing.wbaa.rokku.sts.keycloak

import akka.Done
import akka.actor.ActorSystem
import com.ing.wbaa.rokku.sts.config.KeycloakSettings
import com.ing.wbaa.rokku.sts.data.UserName
import com.ing.wbaa.rokku.sts.helper.OAuth2TokenRequest
import org.scalatest.diagrams.Diagrams
import org.scalatest.wordspec.AsyncWordSpec

import scala.concurrent.ExecutionContextExecutor

class KeycloakClientItTest extends AsyncWordSpec with Diagrams with OAuth2TokenRequest with KeycloakClient {

override implicit val testSystem: ActorSystem = ActorSystem.create("test-system")
override implicit val exContext: ExecutionContextExecutor = testSystem.dispatcher

override val keycloakSettings: KeycloakSettings = new KeycloakSettings(testSystem.settings.config) {
override val realmPublicKeyId: String = "FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"
override val issuerForList: Set[String] = Set("sts-rokku")
}

"Keycloak client" should {
val username = "test"
var createdUserId = KeycloakUserId("")

"add a user" in {
insertUserToKeycloak(UserName(username)).map(addedUserId => {
createdUserId = addedUserId
assert(addedUserId.id.nonEmpty)
})
}

"thrown error when adding existing user" in {
recoverToSucceededIf[javax.ws.rs.WebApplicationException](insertUserToKeycloak(UserName(username)))
}

"delete the created user" in {
deleteUserFromKeycloak(createdUserId).map(d => assert(d == Done))
}
}
}
2 changes: 2 additions & 0 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ rokku {
realm = ${?KEYCLOAK_REALM}
realmPublicKeyId = ${?KEYCLOAK_PUBLIC_KEY_ID}
url = ${?KEYCLOAK_URL}
adminUsername = ${?KEYCLOAK_ADMIN_USERNAME}
adminPassword = ${?KEYCLOAK_ADMIN_PASSWORD}

verifyToken {
checkRealmUrl = ${?KEYCLOAK_CHECK_REALM_URL}
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ rokku {
realm = "auth-rokku"
resource = "sts-rokku"
url = "http://127.0.0.1:8080"

adminUsername = "rokkuadmin"
adminPassword = "password"
verifyToken {
checkRealmUrl = true
issuerForList = ""
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/ing/wbaa/rokku/sts/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package com.ing.wbaa.rokku.sts

import akka.actor.ActorSystem
import com.ing.wbaa.rokku.sts.config._
import com.ing.wbaa.rokku.sts.keycloak.KeycloakTokenVerifier
import com.ing.wbaa.rokku.sts.keycloak.{ KeycloakClient, KeycloakTokenVerifier }
import com.ing.wbaa.rokku.sts.service.{ ExpiredTokenCleaner, UserTokenDbService }
import com.ing.wbaa.rokku.sts.service.db.MariaDb
import com.ing.wbaa.rokku.sts.service.db.dao.{ STSTokenDAO, STSUserAndGroupDAO }
import com.ing.wbaa.rokku.sts.vault.VaultService

object Server extends App {
new RokkuStsService with KeycloakTokenVerifier with UserTokenDbService with STSUserAndGroupDAO with STSTokenDAO with MariaDb with ExpiredTokenCleaner with VaultService {
new RokkuStsService with KeycloakTokenVerifier with UserTokenDbService with STSUserAndGroupDAO with STSTokenDAO with MariaDb with ExpiredTokenCleaner with VaultService with KeycloakClient {
override implicit lazy val system: ActorSystem = ActorSystem.create("rokku-sts")

override protected[this] def httpSettings: HttpSettings = HttpSettings(system)
Expand Down
29 changes: 26 additions & 3 deletions src/main/scala/com/ing/wbaa/rokku/sts/api/AdminApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import akka.http.scaladsl.server.{ AuthorizationFailedRejection, Route }
import com.ing.wbaa.rokku.sts.api.directive.STSDirectives.authorizeToken
import com.ing.wbaa.rokku.sts.config.StsSettings
import com.ing.wbaa.rokku.sts.data.aws.{ AwsAccessKey, AwsCredential, AwsSecretKey }
import com.ing.wbaa.rokku.sts.data.{ AuthenticationUserInfo, BearerToken, NPAAccount, NPAAccountList, RequestId, UserGroup, UserName }
import com.ing.wbaa.rokku.sts.data._
import com.ing.wbaa.rokku.sts.keycloak.KeycloakUserId
import com.ing.wbaa.rokku.sts.service.db.security.Encryption
import com.typesafe.scalalogging.LazyLogging
import com.ing.wbaa.rokku.sts.util.JwtToken
import com.typesafe.scalalogging.LazyLogging

import scala.concurrent.Future
import scala.util.{ Failure, Success }
Expand All @@ -18,7 +19,7 @@ trait AdminApi extends LazyLogging with Encryption with JwtToken {
protected[this] def stsSettings: StsSettings

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

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

protected[this] def getAllNPAAccounts: Future[NPAAccountList]

protected[this] def insertUserToKeycloak(username: UserName): Future[KeycloakUserId]

implicit val requestId = RequestId("")

def userInAdminGroups(userGroups: Set[UserGroup]): Boolean =
Expand Down Expand Up @@ -141,4 +144,24 @@ trait AdminApi extends LazyLogging with Encryption with JwtToken {
}
}

def insertUserToKeycloak: Route = logRequestResult("debug") {
post {
path("keycloak" / "user") {
formFields((Symbol("username"))) { username =>
authorizeToken(verifyAuthenticationToken) { keycloakUserInfo =>
extractUri { uri =>
if (userInAdminGroups(keycloakUserInfo.userGroups)) {
onComplete(insertUserToKeycloak(UserName(username))) {
case Success(_) => complete(ResponseMessage(s"Add user ok", s"$username added", "keycloak"))
case Failure(ex) => complete(ResponseMessage(s"Add user error", ex.getMessage, "keycloak"))
}
} else {
reject(AuthorizationFailedRejection)
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class KeycloakSettings(config: Config) extends Extension {
val checkRealmUrl: Boolean = rokkuStsKeycloakConfig.getBoolean("verifyToken.checkRealmUrl")
val issuerForList: Set[String] =
rokkuStsKeycloakConfig.getString("verifyToken.issuerForList").split(",").map(_.trim).toSet
val adminUsername: String = rokkuStsKeycloakConfig.getString("adminUsername")
val adminPassword: String = rokkuStsKeycloakConfig.getString("adminPassword")
}

object KeycloakSettings extends ExtensionId[KeycloakSettings] with ExtensionIdProvider {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.ing.wbaa.rokku.sts.keycloak

import akka.Done
import com.ing.wbaa.rokku.sts.config.KeycloakSettings
import com.ing.wbaa.rokku.sts.data.UserName
import com.typesafe.scalalogging.LazyLogging
import org.keycloak.OAuth2Constants
import org.keycloak.admin.client.{ CreatedResponseUtil, KeycloakBuilder }
import org.keycloak.representations.idm.UserRepresentation

import scala.concurrent.{ ExecutionContext, Future }

case class KeycloakUserId(id: String)

trait KeycloakClient extends LazyLogging {

implicit protected[this] def executionContext: ExecutionContext

protected[this] def keycloakSettings: KeycloakSettings

private lazy val keycloak = KeycloakBuilder.builder()
.serverUrl(s"${keycloakSettings.url}/auth")
.realm(keycloakSettings.realm)
.grantType(OAuth2Constants.PASSWORD)
.clientId(keycloakSettings.resource)
.username(keycloakSettings.adminUsername)
.password(keycloakSettings.adminPassword)
.build()

/**
* Create a disabled user in a keycloak
*
* @param username npa username
* @return the created user keycloak id
*/
def insertUserToKeycloak(username: UserName): Future[KeycloakUserId] = {

val user = new UserRepresentation()
user.setEnabled(false)
user.setUsername(username.value)
user.setFirstName("added by STS")
user.setLastName("added by STS")

Future {
val response = keycloak.realm(keycloakSettings.realm).users().create(user)
val userId = CreatedResponseUtil.getCreatedId(response)
logger.info("Keycloak add user status {} and Response: {}", response.getStatus, response.getStatusInfo)
logger.info("user {} added to keyckoak - userid={}", username, userId)
KeycloakUserId(userId)
}
}

/**
* delete a user from keycloak
* @param userID - the keycloak user id to delete
* @return Done if no error
*/
def deleteUserFromKeycloak(userID: KeycloakUserId): Future[Done] = {
Future {
keycloak.realm(keycloakSettings.realm).users().delete(userID.id)
logger.info("user {} deleted from keycloak", userID)
Done
}
}

}
23 changes: 23 additions & 0 deletions src/test/scala/com/ing/wbaa/rokku/sts/api/AdminApiTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.auth0.jwt.algorithms.Algorithm
import com.ing.wbaa.rokku.sts.config.StsSettings
import com.ing.wbaa.rokku.sts.data._
import com.ing.wbaa.rokku.sts.data.aws.AwsCredential
import com.ing.wbaa.rokku.sts.keycloak.KeycloakUserId
import org.scalatest.BeforeAndAfterAll
import org.scalatest.diagrams.Diagrams
import org.scalatest.wordspec.AnyWordSpec
Expand Down Expand Up @@ -42,6 +43,11 @@ class AdminApiTest extends AnyWordSpec
override protected[this] def getAllNPAAccounts: Future[NPAAccountList] = Future(NPAAccountList(List(NPAAccount("testNPA", true))))

override protected[this] def insertNpaCredentialsToVault(username: UserName, safeName: String, awsCredential: AwsCredential): Future[Boolean] = Future(true)

protected[this] def insertUserToKeycloak(username: UserName): Future[KeycloakUserId] = username.value match {
case "duplicate" => Future.failed(new RuntimeException("duplicate"))
case _ => Future.successful(KeycloakUserId("1)"))
}
}

private[this] val testRoute: Route = new testAdminApi().adminRoutes
Expand Down Expand Up @@ -117,6 +123,23 @@ class AdminApiTest extends AnyWordSpec
assert(rejections.contains(AuthorizationFailedRejection))
}
}
"return OK when user is in admin groups for adding user to keycloak" in {
Post("/admin/keycloak/user", FormData("username" -> "test1")) ~> validOAuth2TokenHeader ~> testRoute ~> check {
assert(status == StatusCodes.OK)
assert(responseAs[String] == """{"code":"Add user ok","message":"test1 added","target":"keycloak"}""")
}
}
"return Error when user exists for adding user to keycloak" in {
Post("/admin/keycloak/user", FormData("username" -> "duplicate")) ~> validOAuth2TokenHeader ~> testRoute ~> check {
assert(status == StatusCodes.OK)
assert(responseAs[String] == """{"code":"Add user error","message":"duplicate","target":"keycloak"}""")
}
}
"return Rejected when user is not in admin groups for adding user to keycloak" in {
Post("/admin/keycloak/user", FormData("username" -> "test1")) ~> notAdminOAuth2TokenHeader ~> testRoute ~> check {
assert(rejections.contains(AuthorizationFailedRejection))
}
}
}
}
}

0 comments on commit 837ffe8

Please sign in to comment.