Skip to content

Commit

Permalink
Merge pull request #111 from hmrc/GG-7915
Browse files Browse the repository at this point in the history
GG-7915
  • Loading branch information
AadilAkhtar authored Sep 30, 2024
2 parents 4560fe8 + 7270f4c commit fd00a35
Show file tree
Hide file tree
Showing 40 changed files with 588 additions and 467 deletions.
31 changes: 15 additions & 16 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,38 +1,31 @@
version = 3.7.2
runner.dialect = scala213
version = 3.8.3
runner.dialect = scala3
maxColumn = 150
lineEndings = unix
importSelectors = singleLine
project.git = true
binPack.parentConstructors = false
danglingParentheses.callSite = true

rewriteTokens = {
"⇒": "=>"
"→": "->"
"←": "<-"
}

project {
git = true
}

align {
preset = more
tokens = [
":",
"extends",
{code = "=", owner = "Term.Assign"},
{code = "=>", owner = "Case|Type.Arg.ByName"},
"<-", "->", "%", "%%",
"should", "shouldBe", "shouldEqual", "shouldNot", "must"
"<-", "->", "%", "%%", "should", "shouldBe", "shouldEqual", "shouldNot", "must"
]
arrowEnumeratorGenerator = true
openParenCallSite = true
openParenDefnSite = true
closeParenSite = true
}
danglingParentheses.callSite = true

binPack {
parentConstructors = false
}

continuationIndent {
callSite = 2
Expand All @@ -42,16 +35,22 @@ continuationIndent {
newlines {
penalizeSingleSelectMultiArgList = false
sometimesBeforeColonInMethodReturnType = true
afterCurlyLambdaParams = keep
afterCurlyLambdaParams=keep
}

rewrite {
scala3 = {
convertToNewSyntax = true
newSyntax.control = false
removeOptionalBraces = false
}
rules = [RedundantBraces, RedundantParens, AsciiSortImports]
redundantBraces {
maxLines = 100
methodBodies = false
includeUnitMethods = true
stringInterpolation = true
generalExpressions = false
methodBodies = false
}
}

Expand Down
25 changes: 25 additions & 0 deletions PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Description of changes:

---
### PR Checklist
(Taken from standard team checklist maintained in [this confluence page](https://confluence.tools.tax.service.gov.uk/display/GG/PR+Checklist#PRChecklist-ChecklistforPULL_REQUEST_TEMPLATE.md]))

- [ ] Requirements in the associated Jira ticket are met.
- [ ] Request for review has been raised to the team and the ticket status is "Awaiting Review".
- [ ] New work is covered by unit / integration tests.
- [ ] Acceptance & Performance tests are passing and have been updated if required.
- [ ] Combined test coverage is still good (aiming for 85%+)
- [ ] Local configuration if updated is commented and clear.
- [ ] Environment config change PRs have also been raised if required.
- [ ] Naming and coding style of classes/objects/val etc. is descriptive with correctly spellings and consistent with our other services.
- [ ] Comments if added are relevant, clear, concise and helpful.
- [ ] Readme.md is updated if relevant.
- [ ] Confluence documentation is updated if relevant.
- [ ] Dependencies (platform & third party libraries) used are up-to-date, well documented, appropriately licensed and used elsewhere on the platform.
- [ ] There are no avoidable advanced/complex techniques added (e.g. monad transformers)
- [ ] No bespoke technique introduced where a standard platform tool/technique could have been used.
- [ ] No personal identifiable information (PII) is being stored unnecessarily
- [ ] Old and new service instances can be run concurrently without error (i.e. backward compatible)
- [ ] Data format in Mongo hasn't changed or if it has a migration plan has been agreed.
- [ ] Any update to an endpoint interface presented is covered by integration/contract tests and relevant consuming teams are aware.
- [ ] New/changed secrets are documented in Google Docs, agreed with QA engineers and merged into environment config immediately.
8 changes: 4 additions & 4 deletions app/config/APIAccessConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ package config

import com.typesafe.config.{Config, ConfigObject}

import scala.jdk.CollectionConverters._
import scala.jdk.CollectionConverters.*

case class APIAccessConfig(version: String, status: String, accessType: String, endpointsEnabled: Boolean, whiteListedApplicationIds: List[String])

case class APIAccessVersions(versionConfigs: Option[ConfigObject]) {
def findAPIs(versions: List[String], config: Config) = {
def findAPIs(versions: List[String], config: Config): List[APIAccessConfig] = {
versions.map { version =>
val value = config.getConfig(version)

Expand All @@ -34,11 +34,11 @@ case class APIAccessVersions(versionConfigs: Option[ConfigObject]) {
val endpointsEnabled = if (value.hasPath("endpointsEnabled")) value.getBoolean("endpointsEnabled") else false
val versionNumber = version.replace('_', '.')

new APIAccessConfig(versionNumber, status, accessType, endpointsEnabled, allowListedApplicationIds.getOrElse(List()))
APIAccessConfig(versionNumber, status, accessType, endpointsEnabled, allowListedApplicationIds.getOrElse(List()))
}
}

def versions = {
def versions: Option[List[APIAccessConfig]] = {
for {
config <- versionConfigs
keys = config.unwrapped().keySet().asScala.toList
Expand Down
8 changes: 4 additions & 4 deletions app/config/AppContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import uk.gov.hmrc.play.bootstrap.config.ServicesConfig

@Singleton
class AppContext @Inject() (val runModeConfiguration: Configuration) extends ServicesConfig(runModeConfiguration) {
lazy val appName: String = runModeConfiguration.get[String]("appName")
lazy val authUrl: String = baseUrl("auth")
lazy val appName: String = runModeConfiguration.get[String]("appName")
lazy val authUrl: String = baseUrl("auth")
lazy val thirdPartyDelegatedAuthorityUrl: String = baseUrl("third-party-delegated-authority")
lazy val access: Option[ConfigObject] = runModeConfiguration.getOptional[ConfigObject]("api.access.version")
lazy val logUserInfoResponsePayload: Boolean = runModeConfiguration.underlying.getBoolean("log-user-info-response-payload")
lazy val access: Option[ConfigObject] = runModeConfiguration.getOptional[ConfigObject]("api.access.version")
lazy val logUserInfoResponsePayload: Boolean = runModeConfiguration.underlying.getBoolean("log-user-info-response-payload")
}
9 changes: 2 additions & 7 deletions app/config/GuiceModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,17 @@ import com.google.inject.AbstractModule
import com.google.inject.name.Names
import com.typesafe.config.Config
import play.api.{Configuration, Environment}
import uk.gov.hmrc.http.{CorePost, HttpClient, HttpGet}
import connectors._
import connectors.*
import services.{LiveUserInfoService, SandboxUserInfoService, UserInfoService}
import uk.gov.hmrc.play.bootstrap.http.DefaultHttpClient
import uk.gov.hmrc.play.bootstrap.config.ControllerConfig

import scala.annotation.unused

class GuiceModule(val environment: Environment, val configuration: Configuration) extends AbstractModule {
override def configure() = {
override def configure(): Unit = {
bind(classOf[AuthConnector]).to(classOf[AuthConnectorV1])
bind(classOf[HttpClient]).to(classOf[DefaultHttpClient])
bind(classOf[UserInfoService]).annotatedWith(Names.named("live")).to(classOf[LiveUserInfoService])
bind(classOf[UserInfoService]).annotatedWith(Names.named("sandbox")).to(classOf[SandboxUserInfoService])
bind(classOf[CorePost]).to(classOf[DefaultHttpClient])
bind(classOf[HttpGet]).to(classOf[DefaultHttpClient])
bind(classOf[ControllerConfig]).toInstance {
new ControllerConfig {
@unused
Expand Down
33 changes: 16 additions & 17 deletions app/connectors/AuthConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,21 @@
package connectors

import javax.inject.{Inject, Singleton}
import uk.gov.hmrc.auth.core.retrieve.Retrievals
import uk.gov.hmrc.auth.core.retrieve.v2.Retrievals
import uk.gov.hmrc.auth.core.retrieve.~
import uk.gov.hmrc.auth.core.{AuthorisedFunctions, Enrolments, PlayAuthConnector}
import uk.gov.hmrc.http.{CorePost, HeaderCarrier, NotFoundException}
import uk.gov.hmrc.http.{HeaderCarrier, UpstreamErrorResponse}
import domain.{Authority, DesUserInfo, UserDetails}
import config.AppContext
import uk.gov.hmrc.http.client.HttpClientV2

import scala.annotation.nowarn
import scala.concurrent.{ExecutionContext, Future}

@nowarn("cat=deprecation")
abstract class AuthConnector extends PlayAuthConnector with AuthorisedFunctions {
self: UserDetailsFetcher =>

val appContext: AppContext
val http: CorePost
val httpClient: HttpClientV2
val serviceUrl: String = appContext.authUrl

override def authConnector: AuthConnector = this
Expand All @@ -42,39 +41,39 @@ abstract class AuthConnector extends PlayAuthConnector with AuthorisedFunctions
.retrieve(Retrievals.allEnrolments) { enrolments =>
Future.successful(Some(enrolments))
}
.recover { case _: NotFoundException =>
.recover { case UpstreamErrorResponse(_, 404, _, _) =>
None
}
}

def fetchAuthority()(implicit headerCarrier: HeaderCarrier, ec: ExecutionContext): Future[Option[Authority]] = {
authorised()
.retrieve(Retrievals.credentials and Retrievals.nino) {
case credentials ~ nino => Future.successful(Some(Authority(credentials.providerId, nino)))
case _ => Future.successful(None)
case Some(credentials) ~ nino => Future.successful(Some(Authority(credentials.providerId, nino)))
case _ => Future.successful(None)
}
.recover { case _: NotFoundException =>
.recover { case UpstreamErrorResponse(_, 404, _, _) =>
None
}
}

def fetchUserDetails()(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Option[UserDetails]] = self.fetchDetails()(hc, ec)

def fetchDesUserInfo()(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Option[DesUserInfo]] = {
val nothing = Future.successful(None)
authorised()
.retrieve(Retrievals.allItmpUserDetails) {
case name ~ dateOfBirth ~ address =>
Future.successful(Some(DesUserInfo(name, dateOfBirth, address)))
case _ => nothing
.retrieve(Retrievals.allItmpUserDetails) { case name ~ dateOfBirth ~ address =>
Future.successful(Some(DesUserInfo(name, dateOfBirth, address)))
}
.recoverWith { case _: NotFoundException =>
.recoverWith { case UpstreamErrorResponse(_, 404, _, _) =>
nothing
}
}

override def httpClientV2: HttpClientV2 = httpClient

def fetchDetails()(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Option[UserDetails]] = self.fetchUserDetails()
}

@Singleton
class AuthConnectorV1 @Inject() (val appContext: AppContext, val http: CorePost)(implicit val executionContext: ExecutionContext)
class AuthConnectorV1 @Inject() (val appContext: AppContext, val httpClient: HttpClientV2)(implicit val executionContext: ExecutionContext)
extends AuthConnector
with AuthV1UserDetailsFetcher
18 changes: 8 additions & 10 deletions app/connectors/AuthV1UserDetailsFetcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,28 @@
package connectors

import uk.gov.hmrc.auth.core.AuthorisedFunctions
import uk.gov.hmrc.auth.core.retrieve.Retrievals
import uk.gov.hmrc.auth.core.retrieve.v2.Retrievals
import uk.gov.hmrc.auth.core.retrieve.~
import uk.gov.hmrc.http.{HeaderCarrier, NotFoundException}
import domain.UserDetails

import scala.annotation.nowarn
import scala.concurrent.{ExecutionContext, Future}

@nowarn("cat=deprecation")
trait AuthV1UserDetailsFetcher extends UserDetailsFetcher {
self: AuthorisedFunctions =>

def fetchDetails()(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Option[UserDetails]] = {
def fetchUserDetails()(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Option[UserDetails]] = {
authorised()
.retrieve(Retrievals.allUserDetails and Retrievals.mdtpInformation and Retrievals.gatewayInformation) {
case credentials ~ name ~ birthDate ~ postCode ~ email ~ affinityGroup ~ agentCode ~ agentInformation ~
credentialRole ~ description ~ groupId ~ mdtp ~ gatewayInformation =>
credentialRole ~ description ~ groupId ~ mdtp ~ gatewayInformation => {
Future.successful(
Some(
UserDetails(
authProviderId = Some(credentials.providerId),
authProviderType = Some(credentials.providerType),
name = name.name,
lastName = name.lastName,
authProviderId = credentials.map(_.providerId),
authProviderType = credentials.map(_.providerType),
name = name.flatMap(_.name),
lastName = name.flatMap(_.lastName),
dateOfBirth = birthDate,
postCode = postCode,
email = email,
Expand All @@ -58,7 +56,7 @@ trait AuthV1UserDetailsFetcher extends UserDetailsFetcher {
)
)
)
case _ => Future.successful(None)
}
}
.recover { case _: NotFoundException =>
None
Expand Down
21 changes: 13 additions & 8 deletions app/connectors/ThirdPartyDelegatedAuthorityConnector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,27 @@ package connectors
import config.AppContext
import play.api.http.Status
import uk.gov.hmrc.http.HttpReads.Implicits.readRaw
import uk.gov.hmrc.http.{HeaderCarrier, HttpGet}
import uk.gov.hmrc.http.client.HttpClientV2
import uk.gov.hmrc.http.{HeaderCarrier, StringContextOps}

import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class ThirdPartyDelegatedAuthorityConnector @Inject() (appContext: AppContext, http: HttpGet) {
class ThirdPartyDelegatedAuthorityConnector @Inject() (appContext: AppContext, httpClient: HttpClientV2) {
val serviceUrl: String = appContext.thirdPartyDelegatedAuthorityUrl

def fetchScopes(accessToken: String)(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Set[String]] = {
http.GET(s"$serviceUrl/delegated-authority", Nil, Seq("access-token" -> accessToken))(readRaw, hc, ec) map { response =>
if (response.status == Status.NOT_FOUND) {
Set[String]()
} else {
(response.json \ "token" \ "scopes").as[Set[String]]
httpClient
.get(url"$serviceUrl/delegated-authority")
.setHeader("access-token" -> accessToken)
.execute(readRaw, ec)
.map { response =>
if (response.status == Status.NOT_FOUND) {
Set[String]()
} else {
(response.json \ "token" \ "scopes").as[Set[String]]
}
}
}
}
}
2 changes: 1 addition & 1 deletion app/connectors/UserDetailsFetcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ import domain.UserDetails
import scala.concurrent.{ExecutionContext, Future}

trait UserDetailsFetcher {
def fetchDetails()(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Option[UserDetails]]
def fetchUserDetails()(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[Option[UserDetails]]
}
10 changes: 5 additions & 5 deletions app/controllers/DocumentationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@
package controllers

import javax.inject.{Inject, Singleton}
import play.api.http.HttpErrorHandler
import play.api.mvc.{Action, AnyContent, ControllerComponents}
import config.{APIAccessVersions, AppContext}
import views._
import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController
import views.txt

@Singleton
class DocumentationController @Inject() (errorHandler: HttpErrorHandler, appContext: AppContext, assets: Assets, cc: ControllerComponents)
extends uk.gov.hmrc.api.controllers.DocumentationController(cc, assets, errorHandler) {
class DocumentationController @Inject() (appContext: AppContext, assets: Assets, cc: ControllerComponents) extends BackendController(cc) {

override def definition(): Action[AnyContent] = Action {
def definition(): Action[AnyContent] = Action {
val versions = APIAccessVersions(appContext.access)
Ok(txt.definition(versions.versions.getOrElse(List()))).withHeaders("Content-Type" -> "application/json")
}

def ramlDocs(version: String, filename: String): Action[AnyContent] = {
assets.at(s"/public/api/conf/$version", filename)
}

}
20 changes: 19 additions & 1 deletion app/controllers/ErrorResponses.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

package controllers

import play.mvc.Http.Status._
import play.mvc.Http.Status.*
import play.api.libs.json.*

sealed abstract class ErrorResponse(val httpStatusCode: Int, val errorCode: String, val message: String)

Expand All @@ -29,3 +30,20 @@ case class ErrorAcceptHeaderInvalid(msg: String = "The accept header is invalid"
case class ErrorBadGateway(msg: String = "Bad gateway") extends ErrorResponse(BAD_GATEWAY, "BAD_GATEWAY", msg)

case class ErrorBadRequest(msg: String = "Bad request") extends ErrorResponse(BAD_REQUEST, "BAD_REQUEST", msg)

case object ErrorUnauthorizedLowCL extends ErrorResponse(401, "LOW_CONFIDENCE_LEVEL", "Confidence Level on account does not allow access")

object ErrorBadRequest {
def apply(errors: Seq[(JsPath, Seq[JsonValidationError])]): ErrorBadRequest =
ErrorBadRequest(JsError.toJson(errors).as[String])
}

case object ErrorAcceptHeaderInvalid extends ErrorResponse(406, "ACCEPT_HEADER_INVALID", "The accept header is missing or invalid")

case object ErrorInternalServerError extends ErrorResponse(500, "INTERNAL_SERVER_ERROR", "Internal server error")

case object PreferencesSettingsError extends ErrorResponse(500, "PREFERENCE_SETTINGS_ERROR", "Failed to set preferences")

object ErrorResponse {
implicit val writes: Writes[ErrorResponse] = (e: ErrorResponse) => Json.obj("code" -> e.errorCode, "message" -> e.message)
}
Loading

0 comments on commit fd00a35

Please sign in to comment.