Skip to content

Commit 9174864

Browse files
authored
Merge pull request #127 from delphi-hub/feature/userAuth
Feature/user auth
2 parents ff58100 + 3c7e5a9 commit 9174864

24 files changed

+1934
-1471
lines changed

app/authorization/AuthAction.scala

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (C) 2018 The Delphi Team.
2+
// See the LICENCE file distributed with this work for additional
3+
// information regarding copyright ownership.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
package authorization
17+
18+
import javax.inject.Inject
19+
import pdi.jwt._
20+
import play.api.http.HeaderNames
21+
import play.api.mvc._
22+
import authorization.AuthProvider
23+
import play.api.Configuration
24+
25+
import scala.concurrent.{ExecutionContext, Future}
26+
import scala.util.{Failure, Success}
27+
28+
// A custom request type to hold our JWT claims, we can pass these on to the
29+
// handling action
30+
//case class UserRequest[A](jwt: JwtClaim, token: String, request: Request[A]) extends WrappedRequest[A](request)
31+
32+
// Our custom action implementation
33+
class AuthAction @Inject()(bodyParser: BodyParsers.Default)(implicit ec: ExecutionContext, config: Configuration)
34+
extends ActionBuilder[Request, AnyContent] {
35+
36+
override def parser: BodyParser[AnyContent] = bodyParser
37+
override protected def executionContext: ExecutionContext = ec
38+
39+
// A regex for parsing the Authorization header value
40+
private val headerTokenRegex = """Bearer (.+?)""".r
41+
42+
// Called when a request is invoked. We should validate the bearer token here
43+
// and allow the request to proceed if it is valid.
44+
override def invokeBlock[A](request: Request[A], block: Request[A] => Future[Result]): Future[Result] =
45+
extractBearerToken(request) map { token =>
46+
if(AuthProvider.validateJwt(token)) {
47+
block(request) // token was valid - proceed!
48+
} else {
49+
Future.successful(Results.Unauthorized("Invalid")) // token was invalid - return 401
50+
}
51+
} getOrElse Future.successful(Results.Unauthorized) // no token was sent - return 401
52+
53+
// Helper for extracting the token value
54+
private def extractBearerToken[A](request: Request[A]): Option[String] =
55+
request.headers.get(HeaderNames.AUTHORIZATION) collect {
56+
case headerTokenRegex(token) => token
57+
}
58+
}

app/authorization/AuthProvider.scala

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,47 @@
1616
package authorization
1717

1818

19+
1920
import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim}
2021
import play.api.Configuration
2122

22-
object AuthProvider {
2323

24-
var Token = "" // scalastyle:ignore
2524

26-
/** This method generates JWT token for registering Delphi-Management at the Instance-Registry
27-
*
28-
* @param validFor
25+
object AuthProvider {
26+
27+
var Token = "" // scalastyle:ignore
28+
29+
/** This method generates JWT token for registering Delphi-Management at the Instance-Registry
30+
*
31+
* @param validFor
32+
* @return
33+
*/
34+
35+
def generateJwt(validFor: Long = 1)(implicit configuration: Configuration): String = {
36+
val jwtSecretKey = configuration.get[String]("play.http.secret.JWTkey")
37+
if (Token == "" || !Jwt.isValid(Token, jwtSecretKey, Seq(JwtAlgorithm.HS256))) {
38+
val claim = JwtClaim()
39+
.issuedNow
40+
.expiresIn(validFor * 300)
41+
.startsNow
42+
. +("user_id", configuration.get[String]("play.http.instance"))
43+
. +("user_type", "Admin")
44+
45+
Token = Jwt.encode(claim, jwtSecretKey, JwtAlgorithm.HS256)
46+
}
47+
Token
48+
}
49+
50+
/**
51+
* This method receives a token and validates if it is valid
52+
* @param token
53+
* @param configuration
2954
* @return
3055
*/
3156

32-
def generateJwt(validFor: Long = 1)(implicit configuration: Configuration): String = {
57+
def validateJwt(token: String)(implicit configuration: Configuration): Boolean = {
3358
val jwtSecretKey = configuration.get[String]("play.http.secret.JWTkey")
34-
if (Token == "" || !Jwt.isValid(Token, jwtSecretKey, Seq(JwtAlgorithm.HS256))) {
35-
val claim = JwtClaim()
36-
.issuedNow
37-
.expiresIn(validFor * 300)
38-
.startsNow
39-
. +("user_id", configuration.get[String]("play.http.instance"))
40-
. +("user_type", "Admin")
41-
42-
Token = Jwt.encode(claim, jwtSecretKey, JwtAlgorithm.HS256)
43-
}
44-
Token
59+
Jwt.isValid(token, jwtSecretKey, Seq(JwtAlgorithm.HS256)) // Decode the token using the secret key
4560
}
61+
4662
}

app/controllers/ApiRouter.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class ApiRouter @Inject()(irController: InstanceRegistryController, sysControlle
4444
case POST(p"/resumeInstance" ? q"instanceID=$instanceID") => irController.handleRequest(action="/resume", instanceID)
4545
case POST(p"/deleteInstance" ? q"instanceID=$instanceID") => irController.handleRequest(action="/delete", instanceID)
4646
case POST(p"/reconnectInstance" ? q"from=$from"& q"to=$to") => irController.reconnect(from.toInt, to.toInt)
47+
case POST(p"/authenticate") => irController.authentication()
4748
case POST(p"/labelInstance" ? q"instanceID=$instanceID"& q"label=$label") => irController.labelInstance(instanceID, label)
4849
}
4950
}

app/controllers/InstanceRegistryController.scala

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ import akka.actor.{ActorRef, ActorSystem}
2222
import javax.inject.Inject
2323
import play.api.{Configuration, Logger}
2424
import play.api.libs.concurrent.CustomExecutionContext
25-
import play.api.libs.ws.WSClient
25+
import play.api.libs.ws.{WSAuthScheme, WSClient}
2626
import akka.stream.Materializer
2727
import play.api.libs.streams.ActorFlow
2828
import actors.{ClientSocketActor, PublishSocketMessageActor}
2929
import play.api.mvc._
30-
import scala.concurrent.ExecutionContext
31-
import authorization.AuthProvider
32-
import play.api.libs.json.Json
33-
30+
import scala.concurrent.{Future, ExecutionContext}
31+
import authorization.{AuthProvider, AuthAction}
32+
import play.api.libs.json.{Json, JsValue}
3433

3534

3635
trait MyExecutionContext extends ExecutionContext
@@ -55,7 +54,7 @@ class MyExecutionContextImpl @Inject()(implicit system: ActorSystem)
5554

5655
class InstanceRegistryController @Inject()(implicit system: ActorSystem, mat: Materializer, myExecutionContext: MyExecutionContext,
5756
val controllerComponents: ControllerComponents,
58-
ws: WSClient, config: Configuration)
57+
ws: WSClient, config: Configuration, authAction: AuthAction)
5958
extends BaseController {
6059

6160

@@ -64,12 +63,13 @@ class InstanceRegistryController @Inject()(implicit system: ActorSystem, mat: Ma
6463
val instanceRegistryUri = config.get[String]("app.instanceRegistryUri")
6564
val instanceRegistryBasePath = config.get[String]("app.instanceRegistryBasePath")
6665

67-
/**This method maps list of instances with specific componentType.
66+
67+
/** This method maps list of instances with specific componentType.
6868
*
6969
* @param componentType
7070
* @return
7171
*/
72-
def instances(componentType: String): Action[AnyContent] = Action.async {
72+
def instances(componentType: String): Action[AnyContent] = authAction.async {
7373
ws.url(instanceRegistryUri).addQueryStringParameters("ComponentType" -> componentType)
7474
.withHttpHeaders(("Authorization", s"Bearer ${AuthProvider.generateJwt()}"))
7575
.get().map { response =>
@@ -87,13 +87,13 @@ class InstanceRegistryController @Inject()(implicit system: ActorSystem, mat: Ma
8787
}
8888
}
8989

90-
/**Called to fetch network graph of current registry. Contains a list of all instances and all links
90+
/** Called to fetch network graph of current registry. Contains a list of all instances and all links
9191
* currently registered.
9292
*
9393
* @return
9494
*/
9595

96-
def getNetwork(): Action[AnyContent] = Action.async {
96+
def getNetwork(): Action[AnyContent] = authAction.async {
9797
ws.url(instanceRegistryUri + "/instances/network").withHttpHeaders(("Authorization", s"Bearer ${AuthProvider.generateJwt()}"))
9898
.get().map { response =>
9999
// TODO: possible handling of parsing the data can be done here
@@ -114,7 +114,7 @@ class InstanceRegistryController @Inject()(implicit system: ActorSystem, mat: Ma
114114
* @return
115115
*/
116116

117-
def numberOfInstances(componentType: String): Action[AnyContent] = Action.async {
117+
def numberOfInstances(componentType: String): Action[AnyContent] = authAction.async {
118118
// TODO: handle what should happen if the instance registry is not reachable.
119119
// TODO: create constants for the urls
120120
ws.url(instanceRegistryUri + "/count").addQueryStringParameters("ComponentType" -> componentType)
@@ -137,7 +137,7 @@ class InstanceRegistryController @Inject()(implicit system: ActorSystem, mat: Ma
137137
*/
138138

139139

140-
def handleRequest(action: String, instanceID: String): Action[AnyContent] = Action.async { request =>
140+
def handleRequest(action: String, instanceID: String): Action[AnyContent] = authAction.async { request =>
141141
ws.url(instanceRegistryUri + "/instances/" + instanceID + action)
142142
.withHttpHeaders(("Authorization", s"Bearer ${AuthProvider.generateJwt()}"))
143143
.post("")
@@ -146,7 +146,14 @@ class InstanceRegistryController @Inject()(implicit system: ActorSystem, mat: Ma
146146
}(myExecutionContext)
147147
}
148148

149-
def reconnect(from: Int, to: Int): Action[AnyContent] = Action.async { request =>
149+
/**
150+
* This method is called to assign a new instance to the instance with specified ID.
151+
* @param from
152+
* @param to
153+
* @return
154+
*/
155+
156+
def reconnect(from: Int, to: Int): Action[AnyContent] = authAction.async { request =>
150157

151158
ws.url(instanceRegistryUri + "/instances/" + from + "/assignInstance"
152159
)
@@ -161,6 +168,7 @@ class InstanceRegistryController @Inject()(implicit system: ActorSystem, mat: Ma
161168
}
162169
}(myExecutionContext)
163170
}
171+
164172
/**
165173
* This function is for handling an POST request for adding an instance to the Scala web server
166174
* (E.g. .../instances/deploy
@@ -169,25 +177,65 @@ class InstanceRegistryController @Inject()(implicit system: ActorSystem, mat: Ma
169177
* @param name
170178
*/
171179

172-
def postInstance(compType: String, name: String): Action[AnyContent] = Action.async
173-
{
180+
def postInstance(compType: String, name: String): Action[AnyContent] = authAction.async {
174181
request =>
175-
ws.url(instanceRegistryUri + "/instances/deploy")
176-
.withHttpHeaders(("Authorization", s"Bearer ${AuthProvider.generateJwt()}"))
177-
.post(Json.obj("ComponentType" -> compType, "InstanceName" -> name))
178-
.map { response =>
179-
response.status match {
180-
// scalastyle:off magic.number
181-
case 202 =>
182-
// scalastyle:on magic.number
183-
Ok(response.body)
184-
case x: Any =>
185-
new Status(x)
186-
}
187-
}(myExecutionContext)
182+
ws.url(instanceRegistryUri + "/instances/deploy")
183+
.withHttpHeaders(("Authorization", s"Bearer ${AuthProvider.generateJwt()}"))
184+
.post(Json.obj("ComponentType" -> compType, "InstanceName" -> name))
185+
.map { response =>
186+
response.status match {
187+
// scalastyle:off magic.number
188+
case 202 =>
189+
// scalastyle:on magic.number
190+
Ok(response.body)
191+
case x: Any =>
192+
new Status(x)
193+
}
194+
}(myExecutionContext)
188195
}
189196

190-
def labelInstance(instanceID: String, label: String): Action[AnyContent] = Action.async
197+
/**
198+
* This function sends JWT token and Username:Password encoded into the headers to Instance Registry
199+
* Instance registry returns a valid JWT token.
200+
*
201+
* @return
202+
*/
203+
204+
def authentication(): Action[AnyContent] = Action.async {
205+
request =>
206+
//val json = request.body.asJson.get
207+
208+
209+
val jsonBody: Option[JsValue] = request.body.asJson
210+
211+
jsonBody.map { json =>
212+
213+
val username = (json \ "username").as[String]
214+
val password = (json \ "password").as[String]
215+
216+
ws.url(instanceRegistryUri + "/users" + "/authenticate")
217+
.withAuth(username, password, WSAuthScheme.BASIC)
218+
.withHttpHeaders(("Delphi-Authorization", s"${AuthProvider.generateJwt()}"))
219+
.post("")
220+
.map { response =>
221+
if (response.status == 200) {
222+
Ok(Json.obj("token" -> response.body, "refreshToken" -> ""))
223+
} else {
224+
new Status(response.status)
225+
}
226+
}
227+
}.getOrElse{ Future(BadRequest("Invalid body"))}
228+
229+
}
230+
231+
/**
232+
* This function is used to add a label to specific instance.
233+
* @param instanceID ID of the instance on which label is to be added
234+
* @param label
235+
* @return
236+
*/
237+
238+
def labelInstance(instanceID: String, label: String): Action[AnyContent] = authAction.async
191239
{
192240
request =>
193241
ws.url(instanceRegistryUri + "/instances/" + instanceID + "/label")

app/controllers/SystemInfoController.scala

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525
import play.api.libs.json.Json
2626
import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents}
2727
import play.core.PlayVersion
28+
import authorization.AuthAction
29+
import play.api.Configuration
2830

29-
30-
class SystemInfoController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
31+
class SystemInfoController @Inject()(val controllerComponents: ControllerComponents, config: Configuration, authAction: AuthAction) extends BaseController {
3132
implicit val systemInfoWrites = Json.writes[SystemInfo]
3233
implicit val systemInfoReads = Json.reads[SystemInfo]
3334

34-
def getInfo: Action[AnyContent] = Action {
35+
def getInfo: Action[AnyContent] = authAction {
3536

3637
val info = SystemInfo(hostName = getHostName(), javaVersion = getJvmVersion(), platformName = getPlatformName(), scalaVersion = getScalaVersion())
3738
val infoJson = Json.toJson(info)
@@ -44,13 +45,10 @@
4445
}
4546
}
4647

47-
48-
49-
private def getHostName(): String = {
48+
private def getHostName(): String = {
5049
InetAddress.getLocalHost().getHostName()
5150
}
5251

53-
5452
private def getPlatformName(): String = {
5553
val os = "os.name";
5654
val version = "os.version";

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ name := "delphi-management"
55
organization := "de.upb"
66

77

8-
version := "0.9.0"
8+
version := "0.9.5-SNAPSHOT"
99

1010

1111
scalaVersion := "2.12.4"

0 commit comments

Comments
 (0)