Skip to content

Commit

Permalink
Merge pull request #16 from hmrc/PE-2792
Browse files Browse the repository at this point in the history
PE-2792: Add enrolments to userinfo
  • Loading branch information
thegrego authored Mar 22, 2017
2 parents f7edfc3 + b1ac618 commit 5d2c3ee
Show file tree
Hide file tree
Showing 16 changed files with 549 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@

package uk.gov.hmrc.openidconnect.userinfo.connectors

import play.api.Logger
import uk.gov.hmrc.domain.Nino
import uk.gov.hmrc.openidconnect.userinfo.config.WSHttp
import uk.gov.hmrc.openidconnect.userinfo.domain.NinoNotFoundException
import uk.gov.hmrc.openidconnect.userinfo.domain.Enrolment
import uk.gov.hmrc.play.auth.microservice.connectors.ConfidenceLevel._
import uk.gov.hmrc.play.config.ServicesConfig
import uk.gov.hmrc.play.http.{HeaderCarrier, HttpGet}
Expand All @@ -31,18 +32,53 @@ trait AuthConnector extends uk.gov.hmrc.play.auth.microservice.connectors.AuthCo
val http: HttpGet

def confidenceLevel()(implicit hc: HeaderCarrier): Future[Option[Int]] = {
http.GET(s"$authBaseUrl/auth/authority") map { resp =>
(resp.json \ "confidenceLevel").asOpt[Int]
} recover {
case e: Throwable => None
http.GET(s"$authBaseUrl/auth/authority") map { resp =>
(resp.json \ "confidenceLevel").asOpt[Int]
} recover {
case e: Throwable => {
Logger.error(e.getMessage, e)
None
}
}
}

def fetchNino()(implicit hc:HeaderCarrier): Future[Nino] = {
def fetchNino()(implicit hc:HeaderCarrier): Future[Option[Nino]] = {
http.GET(s"$authBaseUrl/auth/authority") map {
resp => (resp.json \ "nino").asOpt[Nino] match {
case Some(n) => n
case None => throw NinoNotFoundException()
resp => (resp.json \ "nino").asOpt[Nino]
} recover {
case e: Throwable => {
Logger.error("Nino not present.", e)
None
}
}
}

def fetchEnrolments()(implicit headerCarrier: HeaderCarrier): Future[Option[Seq[Enrolment]]] = {
getEnrolmentsUri flatMap {
case Some(enrolmentsUri) => {
http.GET(s"$authBaseUrl$enrolmentsUri") map { response =>
response.json.asOpt[Seq[Enrolment]]
} recover {
case e: Throwable => {
Logger.error(e.getMessage, e)
None
}
}
}
case None => {
Logger.debug("No enrolment uri.")
Future.successful(None)
}
}
}

private def getEnrolmentsUri()(implicit hc: HeaderCarrier): Future[Option[String]] = {
http.GET(s"$authBaseUrl/auth/authority").map { response =>
(response.json \ "enrolments").asOpt[String]
} recover {
case e: Throwable => {
Logger.error(e.getMessage, e)
None
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
package uk.gov.hmrc.openidconnect.userinfo.connectors

import play.api.Logger
import uk.gov.hmrc.openidconnect.userinfo.config.{WSHttp, AppContext}
import uk.gov.hmrc.domain.Nino
import uk.gov.hmrc.openidconnect.userinfo.config.{AppContext, WSHttp}
import uk.gov.hmrc.openidconnect.userinfo.domain.DesUserInfo
import uk.gov.hmrc.play.config.ServicesConfig
import uk.gov.hmrc.play.http._
import uk.gov.hmrc.play.http.logging.Authorization

import scala.concurrent.Future

import scala.concurrent.ExecutionContext.Implicits.global

trait DesConnector {
Expand All @@ -36,16 +36,20 @@ trait DesConnector {
val desEnvironment: String
val desBearerToken: String

def fetchUserInfo(nino: String)(implicit hc: HeaderCarrier): Future[Option[DesUserInfo]] = {
def fetchUserInfo(nino: Option[Nino])(implicit hc: HeaderCarrier): Future[Option[DesUserInfo]] = {
val newHc = hc.copy(authorization = Some(Authorization(s"Bearer $desBearerToken"))).withExtraHeaders("Environment" -> desEnvironment)

val url = s"$serviceUrl/pay-as-you-earn/individuals/${withoutSuffix(nino)}"
Logger.debug(s"GET $url with environment=$desEnvironment")
nino map { ninoValue =>
val url = s"$serviceUrl/pay-as-you-earn/individuals/${withoutSuffix(ninoValue.nino)}"
Logger.debug(s"GET $url with environment=$desEnvironment")

http.GET[DesUserInfo](url)(implicitly[HttpReads[DesUserInfo]], newHc) map (Some(_)) recover {
case _: NotFoundException | _: BadRequestException =>
Logger.debug(s"User information for nino $nino is not available in DES")
None
http.GET[DesUserInfo](url)(implicitly[HttpReads[DesUserInfo]], newHc) map (Some(_)) recover {
case _: NotFoundException | _: BadRequestException =>
Logger.debug(s"User information for nino $nino is not available in DES")
None
}
} getOrElse {
Future.successful(None)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package uk.gov.hmrc.openidconnect.userinfo.data

import uk.gov.hmrc.openidconnect.userinfo.domain.{Address, UserInfo}
import uk.gov.hmrc.openidconnect.userinfo.domain.{Address, Enrolment, EnrolmentIdentifier, UserInfo}
import org.joda.time._
import org.scalacheck.Gen

Expand All @@ -29,6 +29,8 @@ trait UserInfoGenerator {
|London
|NW1 9NT
|Great Britain""".stripMargin, Some("NW1 9NT"), Some("Great Britain")))
val enrolments = Seq(Enrolment("IR-SA", List(EnrolmentIdentifier("UTR", "174371121"))))

private lazy val ninoPrefixes = "ABCEGHJKLMNPRSTWXYZ"
private lazy val ninoSuffixes = "ABCD"

Expand Down Expand Up @@ -65,7 +67,7 @@ trait UserInfoGenerator {
middleName <- middleNameGen
dob <- dateOfBirth
nino <- formattedNino
} yield UserInfo(name, lastName, middleName, address, Some(dob), Some(nino))
} yield UserInfo(name, lastName, middleName, address, Some(dob), Some(nino), Some(enrolments))
}

object UserInfoGenerator extends UserInfoGenerator
37 changes: 37 additions & 0 deletions app/uk/gov/hmrc/openidconnect/userinfo/domain/Enrolment.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2017 HM Revenue & Customs
*
* 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 uk.gov.hmrc.openidconnect.userinfo.domain

case class EnrolmentIdentifier(key: String, value: String)

case class Enrolment(key: String,
identifiers: Seq[EnrolmentIdentifier] = Seq(),
state: String = "Activated") {

def getIdentifier(key: String): Option[EnrolmentIdentifier] = identifiers.find { ei =>
ei.key.equalsIgnoreCase(key)
}

def isActivated = state.toLowerCase == "activated"

}

case object ActiveEnrolment {
def unapply(enrolment: Enrolment): Option[String] =
if (enrolment.isActivated) Some(enrolment.key)
else None
}
3 changes: 2 additions & 1 deletion app/uk/gov/hmrc/openidconnect/userinfo/domain/UserInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ case class UserInfo(given_name: Option[String],
middle_name: Option[String],
address: Option[Address],
birthdate: Option[LocalDate],
uk_gov_nino: Option[String])
uk_gov_nino: Option[String],
hmrc_enrolments: Option[Seq[Enrolment]])

case class UserInformation(profile: Option[UserProfile],
address: Option[Address],
Expand Down
57 changes: 37 additions & 20 deletions app/uk/gov/hmrc/openidconnect/userinfo/domain/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,48 @@ import play.api.libs.json._
import play.api.libs.functional.syntax._

package object domain {
implicit val dateReads = Reads.jodaDateReads("yyyy-MM-dd")
implicit val dateWrites = Writes.jodaDateWrites("yyyy-MM-dd")
implicit val addressFmt = Json.format[Address]
implicit val userInfoFmt = Json.format[UserInfo]
implicit val apiAccessFmt = Json.format[APIAccess]

implicit val desUserName : Reads[DesUserName] = (
(JsPath \ "firstForenameOrInitial").readNullable[String] and
(JsPath \ "secondForenameOrInitial").readNullable[String] and
(JsPath \ "surname").readNullable[String]
)(DesUserName.apply _)
(JsPath \ "firstForenameOrInitial").readNullable[String] and
(JsPath \ "secondForenameOrInitial").readNullable[String] and
(JsPath \ "surname").readNullable[String]
)(DesUserName.apply _)

implicit val desAddress : Reads[DesAddress] = (
(JsPath \ "line1").readNullable[String] and
(JsPath \ "line2").readNullable[String] and
(JsPath \ "line3").readNullable[String] and
(JsPath \ "line4").readNullable[String] and
(JsPath \ "postcode").readNullable[String] and
(JsPath \ "countryCode").readNullable[Int]
)(DesAddress.apply _)
(JsPath \ "line1").readNullable[String] and
(JsPath \ "line2").readNullable[String] and
(JsPath \ "line3").readNullable[String] and
(JsPath \ "line4").readNullable[String] and
(JsPath \ "postcode").readNullable[String] and
(JsPath \ "countryCode").readNullable[Int]
)(DesAddress.apply _)

implicit val desUserInfo : Reads[DesUserInfo] = (
(JsPath \ "names" \ "1").read[DesUserName] and
(JsPath \ "dateOfBirth").readNullable[LocalDate] and
(JsPath \ "addresses" \ "1").read[DesAddress]
)(DesUserInfo.apply _)
(JsPath \ "names" \ "1").read[DesUserName] and
(JsPath \ "dateOfBirth").readNullable[LocalDate] and
(JsPath \ "addresses" \ "1").read[DesAddress]
)(DesUserInfo.apply _)

implicit val idformat = Json.format[EnrolmentIdentifier]
implicit val format = Format(
((__ \ "key").read[String] and
(__ \ "identifiers").read[Seq[EnrolmentIdentifier]] and
(__ \ "state").readNullable[String]) {
(key, ids, optState) =>
Enrolment(
key,
ids,
optState.getOrElse("Activate")
)
},
Writes[Enrolment] { enrolment =>
Json.writes[Enrolment].writes(enrolment).as[JsObject]
}
)

implicit val dateReads = Reads.jodaDateReads("yyyy-MM-dd")
implicit val dateWrites = Writes.jodaDateWrites("yyyy-MM-dd")
implicit val addressFmt = Json.format[Address]
implicit val userInfoFmt = Json.format[UserInfo]
implicit val apiAccessFmt = Json.format[APIAccess]
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
package uk.gov.hmrc.openidconnect.userinfo.services

import play.api.Logger
import uk.gov.hmrc.openidconnect.userinfo.connectors.{AuthConnector, DesConnector}
import uk.gov.hmrc.domain.Nino
import uk.gov.hmrc.openidconnect.userinfo.connectors.{AuthConnector, DesConnector, ThirdPartyDelegatedAuthorityConnector}
import uk.gov.hmrc.openidconnect.userinfo.data.UserInfoGenerator
import uk.gov.hmrc.openidconnect.userinfo.domain._
import uk.gov.hmrc.play.http.HeaderCarrier
import scala.concurrent.ExecutionContext.Implicits.global
import uk.gov.hmrc.play.http.logging.Authorization
import uk.gov.hmrc.play.http.{HeaderCarrier, UnauthorizedException}

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Future, Promise}
import scala.util.{Failure, Success}

trait UserInfoService {
def fetchUserInfo()(implicit hc: HeaderCarrier): Future[Option[UserInfo]]
Expand All @@ -33,19 +36,57 @@ trait LiveUserInfoService extends UserInfoService {
val authConnector: AuthConnector
val desConnector: DesConnector
val userInfoTransformer: UserInfoTransformer
val thirdPartyDelegatedAuthorityConnector: ThirdPartyDelegatedAuthorityConnector


override def fetchUserInfo()(implicit hc: HeaderCarrier): Future[Option[UserInfo]] = {
def bearerToken(authorization: Authorization) = augmentString(authorization.value).stripPrefix("Bearer ")

def scopes = hc.authorization match {
case Some(authorization) => thirdPartyDelegatedAuthorityConnector.fetchScopes(bearerToken(authorization))
case None => Future.failed(new UnauthorizedException("Bearer token is required"))
}

val promiseNino = Promise[Option[Nino]]()
val maybeNino = promiseNino.future

scopes flatMap { scopes =>
val scopesForNino = Set("profile", "address", "openid:gov-uk-identifiers")
if ((scopesForNino -- scopes).size != scopesForNino.size) {
authConnector.fetchNino() onComplete {
case Success(nino) => promiseNino success nino
case Failure(exception) => throw exception
}
} else promiseNino success None

val promiseDesUserInfo = Promise[Option[DesUserInfo]]
val maybeDesUserInfo = promiseDesUserInfo.future

val scopesDes = Set("profile", "address")
if ((scopesDes -- scopes).size != scopesDes.size) {
maybeNino onComplete {
case Success(hopefulyNino) => desConnector.fetchUserInfo(hopefulyNino) onComplete {
case Success(desUserInfo) => promiseDesUserInfo success desUserInfo
case Failure(exception) => throw exception
}
case Failure(exception) => throw exception
}
} else promiseDesUserInfo success None

def maybeEnrolments = if (scopes.contains("openid:hmrc_enrolments")) authConnector.fetchEnrolments() else Future.successful(None)

val future: Future[UserInfo] = for {
nino <- authConnector.fetchNino()
desUserInfo <- desConnector.fetchUserInfo(nino.nino)
userInfo <- userInfoTransformer.transform(desUserInfo, nino.nino)
} yield userInfo
val future: Future[UserInfo] = for {
nino <- maybeNino
enrolments <- maybeEnrolments
desUserInfo <- maybeDesUserInfo
} yield userInfoTransformer.transform(scopes, desUserInfo, nino, enrolments)

future map (Some(_)) recover {
case NinoNotFoundException() =>
Logger.debug("Nino not present in Bearer Token")
None
future map (Some((_: UserInfo))) recover {
case e: Throwable => {
Logger.debug(e.getMessage, e)
None
}
}
}
}
}
Expand All @@ -62,6 +103,7 @@ object LiveUserInfoService extends LiveUserInfoService {
override val desConnector = DesConnector
override val authConnector = AuthConnector
override val userInfoTransformer = UserInfoTransformer
override val thirdPartyDelegatedAuthorityConnector = ThirdPartyDelegatedAuthorityConnector
}

object SandboxUserInfoService extends SandboxUserInfoService {
Expand Down
Loading

0 comments on commit 5d2c3ee

Please sign in to comment.