Skip to content

Commit 9ef2ea9

Browse files
authored
Merge pull request #79 from hmrc/BDOG-356
BDOG-356 Check for dev-ops group for Shuttering
2 parents ecbe17b + 45775c5 commit 9ef2ea9

23 files changed

+419
-268
lines changed

app/uk/gov/hmrc/cataloguefrontend/CatalogueController.scala

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import play.api.libs.json.Json.toJson
2525
import play.api.libs.json.{Format, Json}
2626
import play.api.mvc._
2727
import uk.gov.hmrc.cataloguefrontend.DisplayableTeamMember._
28-
import uk.gov.hmrc.cataloguefrontend.actions.{UmpAuthenticated, VerifySignInStatus}
28+
import uk.gov.hmrc.cataloguefrontend.actions.{UmpAuthActionBuilder, VerifySignInStatus}
2929
import uk.gov.hmrc.cataloguefrontend.connector.RepoType.Library
3030
import uk.gov.hmrc.cataloguefrontend.connector.UserManagementConnector.UMPError
3131
import uk.gov.hmrc.cataloguefrontend.connector._
@@ -63,7 +63,7 @@ class CatalogueController @Inject()(
6363
eventService: EventService,
6464
readModelService: ReadModelService,
6565
verifySignInStatus: VerifySignInStatus,
66-
umpAuthenticated: UmpAuthenticated,
66+
umpAuthActionBuilder: UmpAuthActionBuilder,
6767
userManagementPortalConfig: UserManagementPortalConfig,
6868
mcc: MessagesControllerComponents,
6969
digitalServiceInfoPage: DigitalServiceInfoPage,
@@ -102,31 +102,31 @@ class CatalogueController @Inject()(
102102
.fold(NotFound(toJson(s"owner for $digitalService not found")))(ds => Ok(toJson(ds)))
103103
}
104104

105-
def saveServiceOwner(): Action[AnyContent] = umpAuthenticated.async { implicit request =>
106-
request.body.asJson
107-
.map { payload =>
108-
val serviceOwnerSaveEventData: ServiceOwnerSaveEventData = payload.as[ServiceOwnerSaveEventData]
109-
val serviceOwnerDisplayName: String = serviceOwnerSaveEventData.displayName
110-
val optTeamMember: Option[TeamMember] =
111-
readModelService.getAllUsers.find(_.displayName.getOrElse("") == serviceOwnerDisplayName)
112-
113-
optTeamMember.fold {
114-
Future.successful(NotAcceptable(toJson(s"Invalid user: $serviceOwnerDisplayName")))
115-
} { member =>
116-
member.username.fold(
117-
Future.successful(ExpectationFailed(toJson(s"Username was not set (by UMP) for $member!")))) {
118-
serviceOwnerUsername =>
119-
eventService
120-
.saveServiceOwnerUpdatedEvent(
121-
ServiceOwnerUpdatedEventData(serviceOwnerSaveEventData.service, serviceOwnerUsername))
122-
.map(_ => Ok(toJson(DisplayableTeamMember(member, userManagementProfileBaseUrl))))
105+
def saveServiceOwner(): Action[AnyContent] =
106+
umpAuthActionBuilder.whenAuthenticated.async { implicit request =>
107+
request.body.asJson
108+
.map { payload =>
109+
val serviceOwnerSaveEventData: ServiceOwnerSaveEventData = payload.as[ServiceOwnerSaveEventData]
110+
val serviceOwnerDisplayName: String = serviceOwnerSaveEventData.displayName
111+
val optTeamMember: Option[TeamMember] =
112+
readModelService.getAllUsers.find(_.displayName.getOrElse("") == serviceOwnerDisplayName)
113+
114+
optTeamMember.fold {
115+
Future.successful(NotAcceptable(toJson(s"Invalid user: $serviceOwnerDisplayName")))
116+
} { member =>
117+
member.username.fold(
118+
Future.successful(ExpectationFailed(toJson(s"Username was not set (by UMP) for $member!")))) {
119+
serviceOwnerUsername =>
120+
eventService
121+
.saveServiceOwnerUpdatedEvent(
122+
ServiceOwnerUpdatedEventData(serviceOwnerSaveEventData.service, serviceOwnerUsername))
123+
.map(_ => Ok(toJson(DisplayableTeamMember(member, userManagementProfileBaseUrl))))
124+
}
123125
}
124126
}
125-
}
126-
.getOrElse(Future.successful(BadRequest(toJson(s"""Unable to parse json: "${request.body.asText
127-
.getOrElse("No text in request body!")}""""))))
128-
129-
}
127+
.getOrElse(Future.successful(BadRequest(
128+
toJson(s"""Unable to parse json: "${request.body.asText.getOrElse("No text in request body!")}""""))))
129+
}
130130

131131
def allTeams(): Action[AnyContent] = Action.async { implicit request =>
132132
import SearchFiltering._
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2019 HM Revenue & Customs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package uk.gov.hmrc.cataloguefrontend.actions
18+
19+
import cats.data.EitherT
20+
import cats.implicits._
21+
import javax.inject.{Inject, Singleton}
22+
import play.api.mvc._
23+
import uk.gov.hmrc.cataloguefrontend.{ routes => appRoutes }
24+
import uk.gov.hmrc.cataloguefrontend.connector.UserManagementAuthConnector
25+
import uk.gov.hmrc.cataloguefrontend.connector.UserManagementAuthConnector.{UmpToken, User}
26+
import uk.gov.hmrc.cataloguefrontend.service.CatalogueErrorHandler
27+
import uk.gov.hmrc.http.Token
28+
import uk.gov.hmrc.play.HeaderCarrierConverter
29+
import play.api.mvc.Results._
30+
import uk.gov.hmrc.http.HeaderCarrier
31+
32+
import scala.concurrent.{ExecutionContext, Future}
33+
34+
final case class UmpAuthenticatedRequest[A](request: Request[A], token: Token)
35+
extends WrappedRequest(request)
36+
37+
38+
/** Creates an Action will only proceed to invoke the action body, if there is a valid [[UmpToken]] in session.
39+
* If there isn't, it will short circuit with a Redirect to SignIn page.
40+
*
41+
* Use [[VerifySignInStatus]] Action if you want to know if there is a valid token, but it should not terminate invocation.
42+
*/
43+
@Singleton
44+
class UmpAuthActionBuilder @Inject()(
45+
userManagementAuthConnector: UserManagementAuthConnector,
46+
cc : MessagesControllerComponents,
47+
catalogueErrorHandler : CatalogueErrorHandler
48+
)(implicit val ec: ExecutionContext) {
49+
50+
val whenAuthenticated =
51+
withCheck(None)
52+
53+
def withGroup(group: String) =
54+
withCheck(optGroup = Some(group))
55+
56+
private def withCheck(optGroup: Option[String]) =
57+
new ActionBuilder[UmpAuthenticatedRequest, AnyContent]
58+
with ActionRefiner[Request, UmpAuthenticatedRequest] {
59+
60+
def refine[A](request: Request[A]): Future[Either[Result, UmpAuthenticatedRequest[A]]] = {
61+
implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromHeadersAndSession(request.headers, None)
62+
(for {
63+
token <- EitherT.fromOption[Future](request.session.get(UmpToken.SESSION_KEY_NAME)
64+
, Redirect(appRoutes.AuthController.showSignInPage(targetUrl = Some(request.target.uriString).filter(_ => request.method == "GET")))
65+
)
66+
user <- EitherT.fromOptionF[Future, Result, User](userManagementAuthConnector.getUser(UmpToken(token))
67+
, Redirect(appRoutes.AuthController.showSignInPage(targetUrl = Some(request.target.uriString).filter(_ => request.method == "GET")))
68+
)
69+
_ <- if (optGroup.map(user.groups.contains(_)).getOrElse(true))
70+
EitherT.pure[Future, Result](())
71+
else EitherT.left[Result](Future(Forbidden(catalogueErrorHandler.forbiddenTemplate(request))))
72+
} yield UmpAuthenticatedRequest(request, token = Token(token))
73+
).value
74+
}
75+
76+
override def parser: BodyParser[AnyContent] = cc.parsers.anyContent
77+
78+
override protected def executionContext: ExecutionContext = cc.executionContext
79+
}
80+
}

app/uk/gov/hmrc/cataloguefrontend/actions/UmpAuthenticated.scala

Lines changed: 0 additions & 62 deletions
This file was deleted.

app/uk/gov/hmrc/cataloguefrontend/actions/VerifySignInStatus.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final case class UmpVerifiedRequest[A](request: Request[A], override val message
3232
/** Creates an Action to check if there is a UmpToken, and if it is valid.
3333
* It will continue to invoke the action body, with a [[UmpVerifiedRequest]] representing this status.
3434
*
35-
* Use [[UmpAuthenticated]] Action if it should only proceed when there is a valid UmpToken.
35+
* Use [[UmpAuthActionBuilder]] Action if it should only proceed when there is a valid UmpToken.
3636
*/
3737
@Singleton
3838
class VerifySignInStatus @Inject()(
@@ -47,8 +47,8 @@ class VerifySignInStatus @Inject()(
4747

4848
request.session.get(UmpToken.SESSION_KEY_NAME) match {
4949
case Some(token) =>
50-
userManagementAuthConnector.isValid(UmpToken(token)).flatMap { isValid =>
51-
block(UmpVerifiedRequest(request, cc.messagesApi, isSignedIn = isValid))
50+
userManagementAuthConnector.getUser(UmpToken(token)).flatMap { optUser =>
51+
block(UmpVerifiedRequest(request, cc.messagesApi, isSignedIn = optUser.isDefined))
5252
}
5353
case None =>
5454
block(UmpVerifiedRequest(request, cc.messagesApi, isSignedIn = false))

app/uk/gov/hmrc/cataloguefrontend/connector/UserManagementAuthConnector.scala

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,20 @@ class UserManagementAuthConnector @Inject()(
6565
)
6666
}
6767

68-
def isValid(umpToken: UmpToken)(implicit headerCarrier: HeaderCarrier): Future[Boolean] = {
69-
val responseReads: HttpReads[Boolean] = new HttpReads[Boolean] {
70-
def read(method: String, url: String, response: HttpResponse): Boolean =
68+
def getUser(umpToken: UmpToken)(implicit headerCarrier: HeaderCarrier): Future[Option[User]] = {
69+
val responseReads = new HttpReads[Option[User]] {
70+
def read(method: String, url: String, response: HttpResponse): Option[User] =
7171
response.status match {
72-
case OK => true
73-
case UNAUTHORIZED | FORBIDDEN => false
72+
case OK => val groups = (response.json \ "groups").as[List[String]]
73+
Some(User(groups))
74+
case UNAUTHORIZED | FORBIDDEN => None
7475
case other => throw new BadGatewayException(s"Received $other from $method to $url")
7576
}
7677
}
7778

7879
val headerCarrierWithToken = headerCarrier.withExtraHeaders("Token" -> umpToken.value)
7980
http.GET(s"$baseUrl/v1/login")(responseReads, headerCarrierWithToken, implicitly[ExecutionContext])
8081
}
81-
8282
}
8383

8484
@Singleton
@@ -112,4 +112,8 @@ object UserManagementAuthConnector {
112112
type UmpUnauthorized = UmpUnauthorized.type
113113
case object UmpUnauthorized
114114

115+
final case class User(
116+
groups: List[String]
117+
)
118+
115119
}

app/uk/gov/hmrc/cataloguefrontend/service/CatalogueErrorHandler.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ class CatalogueErrorHandler @Inject()(val messagesApi: MessagesApi) extends Fron
3333
override def notFoundTemplate(implicit request: Request[_]): Html =
3434
error_404_template()
3535

36+
def forbiddenTemplate(implicit request: Request[_]): Html =
37+
error_403_template()
38+
3639
}

app/uk/gov/hmrc/cataloguefrontend/service/RouteRulesService.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ class RouteRulesService @Inject()(
3232
)
3333

3434
def serviceUrl(serviceName: String, environment: String = "production")(implicit hc: HeaderCarrier): Future[Option[EnvironmentRoute]] =
35-
routeRulesConnector.serviceRoutes(serviceName).map(environmentRoutes => {
35+
routeRulesConnector.serviceRoutes(serviceName).map(environmentRoutes =>
3636
environmentRoutes
3737
.find(environmentRoute => environmentRoute.environment == environment)
38-
})
38+
)
3939
}
4040

4141
@Singleton

0 commit comments

Comments
 (0)