Skip to content

Commit 779c36f

Browse files
author
Johannes Duesing
committed
Merge develop into feature/traefikIntegration
2 parents 028ff65 + d77a3cf commit 779c36f

File tree

16 files changed

+576
-50
lines changed

16 files changed

+576
-50
lines changed

OpenAPISpecification.yaml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ tags:
2020
containers
2121
- name: Docker Operations
2222
description: Operations on instances that are running in a docker container
23+
- name: User Management
24+
description: Operations related to the user database of Delphi-Management
2325
schemes:
2426
- https
2527
- http
@@ -43,6 +45,81 @@ paths:
4345
TraefikProxyUri:
4446
type: string
4547
example: "172.0.2.1:80"
48+
/users/authenticate:
49+
post:
50+
tags:
51+
- User Management
52+
summary: Authenticates a user and returns a valid JWT
53+
description: >-
54+
This endpoints validates the username and password that must
55+
be supplied in the Authorization header (using HTTP Basic Authentication).
56+
If valid, a JSON Web Token will be generated and returned, that may be used
57+
to authenticate the user for subsequent requests.
58+
operationId: authenticate
59+
parameters:
60+
- in: header
61+
name: Delphi-Authorization
62+
description: >-
63+
Valid JWT that autenticates the calling entity.
64+
type: string
65+
required: true
66+
- in: header
67+
name: Authorization
68+
description: >-
69+
HTTP Basic Authentication following the schema 'Basic <User:Password>
70+
where the concatination of username and password is Base64-Encoded.
71+
type: string
72+
required: true
73+
responses:
74+
'200':
75+
description: Supplied data is valid, a JWT is returned
76+
schema:
77+
type: string
78+
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
79+
'401':
80+
description: Unauthorized, invalid username / password supplied.
81+
/users/add:
82+
post:
83+
tags:
84+
- User Management
85+
summary: Adds a new users for the registry
86+
description: >-
87+
Adds a new user that is passed in the requests entity. The id of the user
88+
will be returned.
89+
operationId: addUser
90+
parameters:
91+
- in: body
92+
name: DelphiUser
93+
description: The user to add
94+
required: true
95+
schema:
96+
type: object
97+
required:
98+
- userName
99+
- secret
100+
- userType
101+
properties:
102+
userName:
103+
type: string
104+
example: MyUser
105+
secret:
106+
type: string
107+
example: 123Pass
108+
userType:
109+
type: string
110+
enum:
111+
- Admin
112+
- User
113+
- Component
114+
responses:
115+
'200':
116+
description: OK, user has been added, id is returned
117+
schema:
118+
type: integer
119+
format: int64
120+
example: 42
121+
'400':
122+
description: Bad request, name already exists
46123
/instances/register:
47124
post:
48125
tags:

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,19 @@ Before you can start the application, you have to make sure your configuration f
7777
|```dockerOperationTimeout``` | ```Timeout``` | ```Timeout(20 seconds)``` | Default timeout for docker operations. If any of the async Docker operations (deploy, stop, pause, ..) takes longer than this, it will be aborted.|
7878
|```dockerUri``` | ```String``` | ```http://localhost:9095``` | Default uri to connect to docker. It will be used if the environment variable ```DELPHI_DOCKER_HOST``` is not specified.|
7979
|```jwtSecretKey``` | ```String``` | ```changeme``` | Secret key to use for JWT signature (HS256). This setting can be overridden by specifying the ```JWT_SECRET``` environment variable.|
80-
|```useInMemoryDB``` | ```Boolean``` | ```true``` | If set to true, all instance data will be kept in memory instead of using a MySQL database.|
81-
|```databaseHost``` | ```String``` | ```"jdbc:mysql://localhost/"``` | Host that the MySQL database is reachable at (only necessary if *useInMemoryDB* is false).|
82-
|```databaseName``` | ```String``` | ```""``` | Name of the MySQL database to use (only necessary if *useInMemoryDB* is false).|
83-
|```databaseDriver``` | ```String``` | ```"com.mysql.jdbc.Driver"``` | Driver to use for the MySQL connection (only necessary if *useInMemoryDB* is false).|
84-
|```databaseUsername``` | ```String``` | ```""``` | Username to use for the MySQL connection (only necessary if *useInMemoryDB* is false).|
85-
|```databasePassword``` | ```String``` | ```""``` | Password to use for the MySQL connection (only necessary if *useInMemoryDB* is false).|
80+
|```useInMemoryInstanceDB``` | ```Boolean``` | ```true``` | If set to true, all instance data will be kept in memory instead of using a MySQL database.|
81+
|```instanceDatabaseHost``` | ```String``` | ```"jdbc:mysql://localhost/"``` | Host that the MySQL instance database is reachable at (only necessary if *useInMemoryInstanceDB* is false).|
82+
|```instanceDatabaseName``` | ```String``` | ```""``` | Name of the MySQL instance database to use (only necessary if *useInMemoryInstanceDB* is false).|
83+
|```instanceDatabaseDriver``` | ```String``` | ```"com.mysql.jdbc.Driver"``` | Driver to use for the MySQL connection (only necessary if *useInMemoryInstanceDB* is false).|
84+
|```instanceDatabaseUsername``` | ```String``` | ```""``` | Username to use for the MySQL instance DB connection (only necessary if *useInMemoryInstanceDB* is false).|
85+
|```instanceDatabasePassword``` | ```String``` | ```""``` | Password to use for the MySQL instance DB connection (only necessary if *useInMemoryInstanceDB* is false).|
86+
|```useInMemoryAuthDB``` | ```Boolean``` | ```true``` | If set to true, all user data will be kept in memory instead of using a MySQL database.|
87+
|```authDatabaseHost``` | ```String``` | ```"jdbc:mysql://localhost/"``` | Host that the MySQL users database is reachable at (only necessary if *useInMemoryAuthDB* is false).|
88+
|```authDatabaseName``` | ```String``` | ```""``` | Name of the MySQL user database to use (only necessary if *useInMemoryAuthDB* is false).|
89+
|```authDatabaseDriver``` | ```String``` | ```"com.mysql.jdbc.Driver"``` | Driver to use for the MySQL users DB connection (only necessary if *useInMemoryAuthDB* is false).|
90+
|```authDatabaseUsername``` | ```String``` | ```""``` | Username to use for the MySQL users DB connection (only necessary if *useInMemoryAuthDB* is false).|
91+
|```authDatabasePassword``` | ```String``` | ```""``` | Password to use for the MySQL users DB connection (only necessary if *useInMemoryAuthDB* is false).|
92+
|```authenticationValidFor``` | ```Int``` | ```30``` | Default duration that user tokens are valid for (in minutes).|
8693
|```maxTotalNoRequest``` | ```Int``` | ```2000``` | Maximum number of requests that are allowed to be executed during the current refresh period regardless of their origin.|
8794
|```maxIndividualIpReq``` | ```Int``` | ```200``` | Maximum number of requests that are allowed to be executed during the current refresh period for one specific origin ip.|
8895
|```ipLogRefreshRate``` | ```FiniteDuration``` | ```2.minutes``` | Duration of the log refresh period.|
@@ -157,7 +164,7 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1NDcxMDYzOTksIm5iZiI6MTU0NzEwNjM
157164
Using the above token, a valid call to the registry at ```localhost:8087``` using *curl* looks like this:
158165

159166
```
160-
curl -X POST -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1NDcxMDYzOTksIm5iZiI6MTU0NzEwNjM5OSwiZXhwIjoxNTU0MDE0Nzk5LCJ1c2VyX2lkIjoiRGVidWdVc2VyIiwidXNlcl90eXBlIjoiQWRtaW4ifQ.TeDa8JkFANVEufPaxXv3AXSojcaiKdOlBKeU5cLaHpg" localhost:8087/deploy?ComponentType=WebApi
167+
curl -X POST -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1NDcxMDYzOTksIm5iZiI6MTU0NzEwNjM5OSwiZXhwIjoxNTU0MDE0Nzk5LCJ1c2VyX2lkIjoiRGVidWdVc2VyIiwidXNlcl90eXBlIjoiQWRtaW4ifQ.TeDa8JkFANVEufPaxXv3AXSojcaiKdOlBKeU5cLaHpg" -H "Content-type: application/json" -d '{"ComponentType":"WebApi"}' localhost:8087/instances/deploy
161168
```
162169

163170
## Contributing

src/main/scala/de/upb/cs/swt/delphi/instanceregistry/Configuration.scala

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,23 @@ class Configuration( ) {
4242
val jwtSecretKey: String = sys.env.getOrElse("JWT_SECRET", "changeme")
4343

4444
//Database configurations
45-
val useInMemoryDB = true
46-
val databaseHost = "jdbc:mysql://localhost/"
47-
val databaseName = ""
48-
val databaseDriver = "com.mysql.jdbc.Driver"
49-
val databaseUsername = ""
50-
val databasePassword = ""
45+
val useInMemoryInstanceDB = true
46+
val instanceDatabaseHost = "jdbc:mysql://localhost/"
47+
val instanceDatabaseName = ""
48+
val instanceDatabaseDriver = "com.mysql.jdbc.Driver"
49+
val instanceDatabaseUsername = ""
50+
val instanceDatabasePassword = ""
51+
52+
//Auth database configuration
53+
val useInMemoryAuthDB = true
54+
val authDatabaseHost = "jdbc:mysql://localhost/"
55+
val authDatabaseName = ""
56+
val authDatabaseDriver = "com.mysql.jdbc.Driver"
57+
val authDatabaseUsername = ""
58+
val authDatabasePassword = ""
59+
60+
//Authentication valid for the time
61+
val authenticationValidFor = 30 //minutes
5162

5263
//Request Limiter
5364
val maxTotalNoRequest: Int = 2000

src/main/scala/de/upb/cs/swt/delphi/instanceregistry/Registry.scala

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import akka.actor.ActorSystem
44
import akka.stream.ActorMaterializer
55
import de.upb.cs.swt.delphi.instanceregistry.Docker._
66
import de.upb.cs.swt.delphi.instanceregistry.connection.Server
7-
import de.upb.cs.swt.delphi.instanceregistry.daos.{DatabaseInstanceDAO, DynamicInstanceDAO, InstanceDAO}
7+
import de.upb.cs.swt.delphi.instanceregistry.daos._
88

99
import scala.concurrent.ExecutionContext
1010
import scala.language.postfixOps
@@ -18,14 +18,22 @@ object Registry extends AppLogging {
1818
val configuration = new Configuration()
1919

2020
private val dao : InstanceDAO = {
21-
if (configuration.useInMemoryDB) {
21+
if (configuration.useInMemoryInstanceDB) {
2222
new DynamicInstanceDAO(configuration)
2323
} else {
2424
new DatabaseInstanceDAO(configuration)
2525
}
2626
}
2727

28-
private val requestHandler = new RequestHandler(configuration, dao, DockerConnection.fromEnvironment(configuration))
28+
private val authDao: AuthDAO = {
29+
if (configuration.useInMemoryAuthDB) {
30+
new DynamicAuthDAO(configuration)
31+
} else {
32+
new DatabaseAuthDAO(configuration)
33+
}
34+
}
35+
36+
private val requestHandler = new RequestHandler(configuration, authDao, dao, DockerConnection.fromEnvironment(configuration))
2937

3038
private val server: Server = new Server(requestHandler)
3139

src/main/scala/de/upb/cs/swt/delphi/instanceregistry/RequestHandler.scala

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import akka.stream.{ActorMaterializer, Materializer, OverflowStrategy}
99
import akka.util.Timeout
1010
import de.upb.cs.swt.delphi.instanceregistry.Docker.DockerActor._
1111
import de.upb.cs.swt.delphi.instanceregistry.Docker.{ContainerAlreadyStoppedException, DockerActor, DockerConnection}
12+
import de.upb.cs.swt.delphi.instanceregistry.authorization.AuthProvider
1213
import de.upb.cs.swt.delphi.instanceregistry.connection.RestClient
13-
import de.upb.cs.swt.delphi.instanceregistry.daos.InstanceDAO
14+
import de.upb.cs.swt.delphi.instanceregistry.daos.{AuthDAO, InstanceDAO}
1415
import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.InstanceEnums.{ComponentType, InstanceState}
1516
import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.LinkEnums.LinkState
1617
import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model._
@@ -20,13 +21,15 @@ import scala.concurrent.{Await, ExecutionContext, Future}
2021
import scala.language.postfixOps
2122
import scala.util.{Failure, Success, Try}
2223

23-
class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, connection: DockerConnection) extends AppLogging {
24+
class RequestHandler(configuration: Configuration, authDao: AuthDAO, instanceDao: InstanceDAO, connection: DockerConnection) extends AppLogging {
2425

2526

2627
implicit val system: ActorSystem = Registry.system
2728
implicit val materializer: Materializer = ActorMaterializer()
2829
implicit val ec: ExecutionContext = system.dispatcher
2930

31+
val authProvider: AuthProvider = new AuthProvider(authDao)
32+
3033
val (eventActor, eventPublisher) = Source.actorRef[RegistryEvent](10, OverflowStrategy.dropNew)
3134
.toMat(Sink.asPublisher(fanout = true))(Keep.both)
3235
.run()
@@ -35,6 +38,7 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con
3538
def initialize(): Unit = {
3639
log.info("Initializing request handler...")
3740
instanceDao.initialize()
41+
authDao.initialize()
3842
if (!instanceDao.allInstances().exists(instance => instance.name.equals("Default ElasticSearch Instance"))) {
3943
//Add default ES instance
4044
handleRegister(Instance(None,
@@ -54,6 +58,7 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con
5458
def shutdown(): Unit = {
5559
eventActor ! PoisonPill
5660
instanceDao.shutdown()
61+
authDao.shutdown()
5762
}
5863

5964
/**
@@ -944,6 +949,24 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con
944949
}
945950
}
946951

952+
/**
953+
* Add user to user database
954+
*
955+
* @param user
956+
* @return
957+
*/
958+
def handleAddUser(user: DelphiUser): Try[Long] = {
959+
960+
val noIdUser = DelphiUser(id = None, userName = user.userName, secret = user.secret, userType = user.userType)
961+
962+
authDao.addUser(noIdUser) match {
963+
case Success(id) =>
964+
log.info(s"Successfully handled create user request")
965+
Success(id)
966+
case Failure(x) => Failure(x)
967+
}
968+
}
969+
947970

948971
def isInstanceIdPresent(id: Long): Boolean = {
949972
instanceDao.hasInstance(id)

src/main/scala/de/upb/cs/swt/delphi/instanceregistry/authorization/AccessToken.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ final case class AccessToken(userId: String,
99
issuedAt: DateTime,
1010
notBefore: DateTime)
1111

12+
1213
object AccessTokenEnums {
1314

1415
type UserType = UserType.Value

src/main/scala/de/upb/cs/swt/delphi/instanceregistry/authorization/AuthProvider.scala

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,82 @@
11
package de.upb.cs.swt.delphi.instanceregistry.authorization
22

3+
import java.nio.charset.StandardCharsets
4+
import java.security.MessageDigest
35
import akka.actor.ActorSystem
46
import akka.http.scaladsl.model.DateTime
57
import akka.http.scaladsl.server.directives.Credentials
68
import de.upb.cs.swt.delphi.instanceregistry.authorization.AccessTokenEnums.UserType
7-
import pdi.jwt.{Jwt, JwtAlgorithm}
9+
import de.upb.cs.swt.delphi.instanceregistry.daos.{AuthDAO, DatabaseAuthDAO}
10+
import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim}
811
import de.upb.cs.swt.delphi.instanceregistry.{AppLogging, Registry}
912
import spray.json._
1013

1114
import scala.util.{Failure, Success, Try}
1215

13-
object AuthProvider extends AppLogging {
16+
class AuthProvider(authDAO: AuthDAO) extends AppLogging {
1417

1518
implicit val system : ActorSystem = Registry.system
1619

20+
def authenticateBasicJWT(credentials: Credentials) : Option[String] = {
21+
credentials match {
22+
case p @ Credentials.Provided(userName) => {
23+
if (getSecretForUser(userName).isEmpty) {
24+
None
25+
} else if(p.verify(getSecretForUser(userName).get, hashString)){
26+
Some(userName)
27+
} else {
28+
None
29+
}
30+
}
31+
case _ => None
32+
}
33+
}
34+
35+
def hashString: String => String = { secret: String =>
36+
MessageDigest.getInstance("SHA-256").digest(secret.getBytes(StandardCharsets.UTF_8)).map("%02x".format(_)).mkString("")
37+
}
38+
39+
private def getSecretForUser(userName: String): Option[String] ={
40+
val user = authDAO.getUserWithUsername(userName)
41+
if(user.isDefined){
42+
Some(user.get.secret)
43+
} else {
44+
None
45+
}
46+
}
47+
48+
def isValidDelphiToken(token: String): Boolean ={
49+
Jwt.decodeRawAll(token, Registry.configuration.jwtSecretKey, Seq(JwtAlgorithm.HS256)) match {
50+
case Success((_, payload, _)) =>
51+
parseDelphiTokenPayload(payload) match {
52+
case Success(user_type) =>
53+
log.info(s"Successfully parsed Delphi Authorization token")
54+
true
55+
case Failure(ex) =>
56+
log.error(ex, s"Failed to parse Delphi Authorization with message ${ex.getMessage}")
57+
false
58+
}
59+
case Failure(ex) =>
60+
log.warning(s"Failed to validate jwt token with message ${ex.getMessage}")
61+
false
62+
}
63+
}
64+
65+
def generateJwt(userName: String): String = {
66+
val validFor: Long = Registry.configuration.authenticationValidFor
67+
val user = authDAO.getUserWithUsername(userName)
68+
69+
val claim = JwtClaim()
70+
.issuedNow
71+
.expiresIn(validFor * 60)
72+
.startsNow
73+
. + ("user_id", user.get.userName)
74+
. + ("user_type", user.get.userType)
75+
76+
val secretKey = Registry.configuration.jwtSecretKey
77+
Jwt.encode(claim, secretKey, JwtAlgorithm.HS256)
78+
}
79+
1780
def authenticateOAuth(credentials: Credentials) : Option[AccessToken] = {
1881
credentials match {
1982
case _ @ Credentials.Provided(tokenString) =>
@@ -77,6 +140,17 @@ object AuthProvider extends AppLogging {
77140
}
78141
}
79142

143+
private def parseDelphiTokenPayload(jwtPayload: String) : Try[(String, String)] = {
144+
Try[(String, String)] {
145+
val json = jwtPayload.parseJson.asJsObject
146+
147+
val user_id = json.fields("user_id").asInstanceOf[JsString].value
148+
val user_type = json.fields("user_type").asInstanceOf[JsString].value
149+
150+
(user_id, user_type)
151+
}
152+
}
153+
80154
private def canAccess(tokenType: UserType, requiredType: UserType) = {
81155
if(tokenType == UserType.Admin){
82156
true

0 commit comments

Comments
 (0)