Skip to content

Commit

Permalink
add new hmda-auth service which allows users to update their keycloak…
Browse files Browse the repository at this point in the history
… account
  • Loading branch information
kgudel committed Aug 17, 2023
1 parent aee319d commit 46b93b2
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 65 deletions.
38 changes: 35 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ lazy val sparkDeps =

lazy val authDeps = Seq(keycloakAdapter, keycloak, keycloakAdmin, jbossLogging, httpClient)

lazy val keycloakServerDeps = Seq(resteasyClient, resteasyJackson, resteasyMulti, keycloakCryptoDefault)
lazy val keycloakServerDeps = Seq(resteasyClient, resteasyJackson, resteasyMulti)

lazy val akkaDeps = Seq(
akkaSlf4J,
Expand Down Expand Up @@ -515,8 +515,40 @@ lazy val `hmda-analytics` = (project in file("hmda-analytics"))
case "META-INF/io.netty.versions.properties" => MergeStrategy.concat
case "META-INF/MANIFEST.MF" => MergeStrategy.discard
case PathList("META-INF", xs @ _*) => MergeStrategy.concat
case PathList("jakarta/ws/rs/core", js @ _*) => MergeStrategy.discard
case PathList("jakarta", ks @ _*) => MergeStrategy.first
//case x if x.endsWith("/ws/rs/core/Configurable.class") => MergeStrategy.filterDistinctLines
//java.lang.ClassFormatError: Incompatible magic value 4022320623 in class file jakarta/ws/rs/core/Configurable
//case x if x.endsWith("/ws/rs/core/Configurable.class") => MergeStrategy.rename
//java.lang.ClassNotFoundException: jakarta.ws.rs.core.Configurable
case x if x.endsWith("/ws/rs/core/Configurable.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/client/ClientBuilder.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/client/Client.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/ext/Providers.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/Configuration.class") => MergeStrategy.last
case x if x.endsWith("ws/rs/ext/RuntimeDelegate.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/ext/MessageBodyWriter.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/ext/MessageBodyReader.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/ext/ExceptionMapper.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/FeatureContext.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/ext/ContextResolver.class") => MergeStrategy.last
case x if x.endsWith("ws/rs/ext/RuntimeDelegate$HeaderDelegate.class") => MergeStrategy.last
case x if x.endsWith("ws/rs/RuntimeType.class") => MergeStrategy.last
case x if x.endsWith("ws/rs/core/MediaType.class") => MergeStrategy.last
case x if x.endsWith("ws/rs/core/NewCookie.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/Cookie.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/EntityTag.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/CacheControl.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/Link.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/ConstrainedTo.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/ext/Provider.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/Consumes.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/Produces.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/Response$ResponseBuilder.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/UriBuilder.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/Variant$VariantListBuilder.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/core/Link$Builder.class") => MergeStrategy.last
case x if x.endsWith("/annotation/Priority.class") => MergeStrategy.last
case x if x.endsWith("/ws/rs/WebApplicationException.class") => MergeStrategy.last
case PathList("jakarta", xs @ _*) => MergeStrategy.last
case "reference.conf" => MergeStrategy.concat
case PathList(ps @ _*) if ps.last endsWith ".proto" =>
MergeStrategy.first
Expand Down
4 changes: 4 additions & 0 deletions common/src/main/scala/hmda/auth/OAuth2Authorization.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import hmda.api.http.model.ErrorResponse
import org.keycloak.adapters.KeycloakDeploymentBuilder
import org.keycloak.representations.adapters.config.AdapterConfig
import org.slf4j.Logger
import org.keycloak.common.crypto.CryptoIntegration

import java.util.concurrent.atomic.AtomicReference
import scala.collection.JavaConverters._
import scala.util.{Failure, Success}
import org.keycloak.common.crypto.CryptoIntegration
// $COVERAGE-OFF$
class OAuth2Authorization(logger: Logger, tokenVerifier: TokenVerifier) {
CryptoIntegration.init(this.getClass.getClassLoader)

private val tokenAttributeRefKey = AttributeKey[AtomicReference[VerifiedToken]]("tokenRef")

Expand Down Expand Up @@ -132,6 +135,7 @@ class OAuth2Authorization(logger: Logger, tokenVerifier: TokenVerifier) {
val verifiedToken = VerifiedToken(
token,
vT.getId,
vT.getSubject,
vT.getName,
vT.getPreferredUsername,
vT.getEmail,
Expand Down
4 changes: 2 additions & 2 deletions common/src/main/scala/hmda/auth/VerifiedToken.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package hmda.auth

case class VerifiedToken(token: String, id: String, name: String, username: String, email: String, roles: Seq[String], lei: String)
case class VerifiedToken(token: String, id: String, userId: String, name: String, username: String, email: String, roles: Seq[String], lei: String)

object VerifiedToken {
def apply(): VerifiedToken =
VerifiedToken("empty-token", "af89acfe-c404-4afb-8c8f-2396a3d06c84", "token", "dev", "dev@dev.com", Seq.empty, "lei")
VerifiedToken("empty-token", "11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222", "token", "dev", "dev@dev.com", Seq.empty, "lei")
}
6 changes: 3 additions & 3 deletions hmda-auth/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ keycloak {
client.id = ${?KEYCLOAK_HMDA_API_CLIENT_ID}
public.key = "AYUeqDHLF_GFsZYOSMXzhBT4zyQS--KiEmBFvMzJrBA"
public.key = ${?KEYCLOAK_PUBLIC_KEY_ID}
auth.server.url = "https://hmda4.demo.cfpb.gov/auth/"
auth.server.url = "https://ffiec.cfpb.gov/auth/"
auth.server.url = ${?KEYCLOAK_AUTH_URL}
hmda.admin.role = "hmda-admin"
hmda.admin.role = ${?KEYCLOAK_HMDA_ADMIN_ROLE}
admin {
username = "keycloak"
username = ${?KEYCLOAK_ADMIN_USR}
username = ${?KEYCLOAK_ADMIN_USERNAME}
password = "keycloak"
password = ${?KEYCLOAK_ADMIN_PWD}
password = ${?KEYCLOAK_ADMIN_PASSWORD}
}
}
88 changes: 35 additions & 53 deletions hmda-auth/src/main/scala/api/AuthHttpApi.scala
Original file line number Diff line number Diff line change
@@ -1,99 +1,81 @@
package hmda.authService.api

import akka.NotUsed
import akka.http.scaladsl.model.{ HttpRequest, HttpResponse, StatusCodes }
import akka.http.scaladsl.marshalling.ToResponseMarshallable
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.StatusCodes.BadRequest
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.directives.RouteDirectives.complete
import akka.http.scaladsl.server.{ Directive, Directive0 }
import akka.http.scaladsl.server.Route
import akka.util.ByteString
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._
import org.slf4j.Logger
import akka.stream.alpakka.s3.scaladsl.S3
import akka.stream.scaladsl.{ Sink, Source }

import scala.concurrent.Future
import akka.stream.alpakka.s3._
import com.typesafe.config.ConfigFactory
import software.amazon.awssdk.auth.credentials.{ AwsBasicCredentials, StaticCredentialsProvider }
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.regions.providers.AwsRegionProvider
import software.amazon.awssdk.regions.providers._
import akka.stream.alpakka.s3.ApiVersion.ListBucketVersion2
import scala.collection.JavaConverters._
import akka.actor.ActorSystem

import slick.basic.DatabaseConfig
import slick.jdbc.JdbcProfile

import scala.collection.JavaConverters._
import scala.concurrent._
import akka.http.scaladsl.model.ContentTypes
import scala.concurrent.Future
import scala.util.{ Failure, Success}

import scala.util.{ Failure, Success, Try }
import akka.http.scaladsl.model.StatusCodes.BadRequest
import hmda.auth.OAuth2Authorization
import akka.util.Timeout
import hmda.util.RealTimeConfig
import hmda.authService.model.UserUpdate
import hmda.institution.query.InstitutionEmailComponent
import hmda.api.http.model.ErrorResponse
import hmda.institution.query.InstitutionEmailComponent

import org.keycloak.admin.client.Keycloak
import org.keycloak.admin.client.KeycloakBuilder
import org.keycloak.admin.client.resource._
import org.keycloak.representations.idm._

import scala.concurrent.duration._

object AuthHttpApi {
def create(log: Logger)(implicit ec: ExecutionContext, system: ActorSystem): OAuth2Authorization => Route = new AuthHttpApi(log).authHttpRoutes _
def create(log: Logger)(implicit ec: ExecutionContext): OAuth2Authorization => Route = new AuthHttpApi(log).authHttpRoutes _
}
private class AuthHttpApi(log: Logger)(implicit ec: ExecutionContext, system: ActorSystem) extends InstitutionEmailComponent{
private class AuthHttpApi(log: Logger)(implicit ec: ExecutionContext) extends InstitutionEmailComponent{

val config = ConfigFactory.load()
val dbConfig = DatabaseConfig.forConfig[JdbcProfile]("institution_db")

val keycloakClientId = config.getString("keycloak.client.id")
val keycloakServerUrl = config.getString("keycloak.auth.server.url")
val keycloakRealm = config.getString("keycloak.realm")
val keycloakAdminUsername = config.getString("keycloak.admin.username")
val keycloakAdminPassword = config.getString("keycloak.admin.password")

implicit val institutionEmailsRepository: InstitutionEmailsRepository = new InstitutionEmailsRepository(dbConfig)

val keycloak = KeycloakBuilder.builder()
.serverUrl("https://hmda4.demo.cfpb.gov/auth/")
.serverUrl(keycloakServerUrl)
.realm("master")
.clientId("admin-cli")
.grantType("password")
.username("keycloak")
.password("keycloak")
.username(keycloakAdminUsername)
.password(keycloakAdminPassword)
.build()

def authHttpRoutes(oAuth2Authorization: OAuth2Authorization): Route = {
encodeResponse {
pathPrefix("auth") {
pathPrefix("user") {
(extractUri & put) { uri =>
oAuth2Authorization.authorizeVerifiedToken() { token =>
entity(as[UserUpdate]){ userUpdate =>
val allUsers: UsersResource = keycloak.realm("hmda2").users()
val userResource: UserResource = allUsers.get(token.id)
val user: UserRepresentation = userResource.toRepresentation()
val fIsValidLeis = verifyLeis(getDomainFromEmail(token.email), userUpdate.leis)
fIsValidLeis.onComplete {
case Success(isValidLeis) =>
if (isValidLeis) user.setAttributes(Map(("lei", List(userUpdate.leis.mkString(",")).asJava)).asJava)
else complete((BadRequest, ErrorResponse(BadRequest.intValue, "LEIs are not authorized for the users email domain", uri.path)))
case Failure(e) =>
complete((BadRequest, ErrorResponse(StatusCodes.InternalServerError.intValue, "Failed to fetch list of authorized LEIs for users email domain", uri.path)))
}
if (userUpdate.firstName != user.getFirstName()) user.setFirstName(userUpdate.firstName)
if (userUpdate.lastName != user.getLastName()) user.setLastName(userUpdate.lastName)
println(user.getAttributes())
userResource.update(user)
complete(HttpResponse(StatusCodes.OK, entity = token.email))
pathPrefix("user") {
(extractUri & put) { uri =>
oAuth2Authorization.authorizeVerifiedToken() { token =>
entity(as[UserUpdate]){ userUpdate =>
val allUsers: UsersResource = keycloak.realm(keycloakRealm).users()
val userResource: UserResource = allUsers.get(token.userId)
val user: UserRepresentation = userResource.toRepresentation()
val fIsValidLeis = verifyLeis(getDomainFromEmail(token.email), userUpdate.leis)
if (userUpdate.firstName != user.getFirstName()) user.setFirstName(userUpdate.firstName)
if (userUpdate.lastName != user.getLastName()) user.setLastName(userUpdate.lastName)
onComplete(fIsValidLeis) {
case Success(isValidLeis) =>
if (isValidLeis) {
user.setAttributes(Map(("lei", List(userUpdate.leis.mkString(",")).asJava)).asJava)
userResource.update(user)
complete(ToResponseMarshallable(userUpdate))
}
else complete((BadRequest, ErrorResponse(BadRequest.intValue, "LEIs are not authorized for the users email domain", uri.path)))
case Failure(e) =>
complete((BadRequest, ErrorResponse(StatusCodes.InternalServerError.intValue, "Failed to fetch list of authorized LEIs for users email domain", uri.path)))
}
}
}
Expand All @@ -106,8 +88,8 @@ private class AuthHttpApi(log: Logger)(implicit ec: ExecutionContext, system: Ac

private def verifyLeis(emailDomain: String, proposedLeis: List[String]): Future[Boolean] = {
findByEmailAnyYear(emailDomain).map { institutions =>
val oldLeis = institutions.map(institution => institution.LEI)
oldLeis.toSet == proposedLeis.toSet
val availableLeis = institutions.map(institution => institution.LEI)
proposedLeis.forall(availableLeis.contains)
}
}
}
3 changes: 0 additions & 3 deletions hmda-auth/src/main/scala/hmdaAuth.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorSystem => ClassicActorSystem}
import akka.stream.Materializer
import akka.util.Timeout
import scala.concurrent.ExecutionContext
import hmda.authService.api.HmdaAuthApi
import org.slf4j.LoggerFactory
import scala.concurrent.duration._

object HmdaAuth extends App {

Expand All @@ -26,7 +24,6 @@ object HmdaAuth extends App {
implicit val system: ActorSystem[_] = classicSystem.toTyped
implicit val materializer: Materializer = Materializer(system)
implicit val ec: ExecutionContext = system.executionContext
implicit val timeout: Timeout = Timeout(1.hour)

ActorSystem[Nothing](HmdaAuthApi(), HmdaAuthApi.name)
}
2 changes: 2 additions & 0 deletions hmda-auth/src/main/scala/model/userUpdate.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package hmda.authService.model

import io.circe.Decoder
import io.circe.Encoder
import io.circe.generic.semiauto._

case class UserUpdate(firstName: String, lastName: String, leis: List[String])

object UserUpdate {
implicit val decoder: Decoder[UserUpdate] = deriveDecoder[UserUpdate]
implicit val encoder: Encoder[UserUpdate] = deriveEncoder[UserUpdate]
}
1 change: 1 addition & 0 deletions kubernetes/config-maps/http-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ metadata:
reloader.stakater.com/match: "true"
data:
auth.realmUrl: "https://ffiec.cfpb.gov/auth/realms/hmda2"
auth.url: "https://ffiec.cfpb.gov/auth/"
census.host: census-api.default.svc.cluster.local
census.port: "9093"
15 changes: 15 additions & 0 deletions kubernetes/hmda-auth/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ spec:
configMapKeyRef:
name: http-configmap
key: auth.realmUrl
- name: KEYCLOAK_AUTH_URL
valueFrom:
configMapKeyRef:
name: http-configmap
key: auth.url
- name: KEYCLOAK_PUBLIC_MODULUS
valueFrom:
configMapKeyRef:
Expand All @@ -62,6 +67,16 @@ spec:
configMapKeyRef:
name: keycloak-public-key-configmap
key: keycloak.publicKey.exponent
- name: KEYCLOAK_ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: keycloak-credentials
key: admin-username
- name: KEYCLOAK_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-credentials
key: admin-password
- name: PG_HOST
valueFrom:
secretKeyRef:
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/hmda-auth/templates/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ metadata:
name: hmda_auth_mapping
ambassador_id: ambassador-default-1
prefix: /hmda-auth/
rewrite: /auth/
rewrite: /
service: {{ include "hmda-auth.fullname" . }}:{{ .Values.service.port }}
timeout_ms: 300000
add_response_headers:
Expand Down

0 comments on commit 46b93b2

Please sign in to comment.