Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F63744647
StewardService.scala
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Wed, May 22, 05:29
Size
13 KB
Mime Type
text/x-c++
Expires
Fri, May 24, 05:29 (2 d)
Engine
blob
Format
Raw Data
Handle
17813305
Attached To
R2664 SHRINE MedCo Fork
StewardService.scala
View Options
package
net.shrine.steward
import
akka.actor.Actor
import
akka.event.Logging
import
net.shrine.authentication.UserAuthenticator
import
net.shrine.authorization.steward._
import
net.shrine.i2b2.protocol.pm.User
import
net.shrine.steward.db._
import
net.shrine.steward.pmauth.Authorizer
import
shapeless.HNil
import
spray.http.
{
HttpRequest
,
HttpResponse
,
StatusCodes
}
import
spray.httpx.Json4sSupport
import
spray.routing.directives.LogEntry
import
spray.routing._
import
org.json4s.
{
DefaultFormats
,
Formats
}
import
scala.concurrent.ExecutionContext.Implicits.global
import
scala.util.
{
Failure
,
Success
,
Try
}
// we don't implement our route structure directly in the service actor because
// we want to be able to test it independently, without having to spin up an actor
class
StewardServiceActor
extends
Actor
with
StewardService
{
// the HttpService trait defines only one abstract member, which
// connects the services environment to the enclosing actor or test
def
actorRefFactory
=
context
// this actor only runs our route, but you could add
// other things here, like request stream processing
// or timeout handling
def
receive
=
runRoute
(
route
)
}
// this trait defines our service behavior independently from the service actor
trait
StewardService
extends
HttpService
with
Json4sSupport
{
implicit
def
json4sFormats
:
Formats
=
DefaultFormats
val
userAuthenticator
=
UserAuthenticator
(
StewardConfigSource
.
config
)
//don't need to do anything special for unauthorized users, but they do need access to a static form.
lazy
val
route
:
Route
=
gruntWatchCorsSupport
{
requestLogRoute
~
fullLogRoute
}
lazy
val
requestLogRoute
=
logRequestResponse
(
logEntryForRequest
_
)
{
redirectToIndex
~
staticResources
~
makeTrouble
~
about
}
lazy
val
fullLogRoute
=
logRequestResponse
(
logEntryForRequestResponse
_
)
{
qepRoute
~
authenticatedInBrowser
}
// logs just the request method, uri and response at info level
//logging is controlled by Akka's config, slf4j, and log4j config
def
logEntryForRequestResponse
(
req
:
HttpRequest
)
:
Any
=>
Option
[
LogEntry
]
=
{
case
res
:
HttpResponse
=>
{
Some
(
LogEntry
(
s
"\n Request: $req \n Response: $res"
,
Logging
.
InfoLevel
))
}
case
_
=>
None
// other kind of responses
}
// logs just the request method, uri and response status at info level
def
logEntryForRequest
(
req
:
HttpRequest
)
:
Any
=>
Option
[
LogEntry
]
=
{
case
res
:
HttpResponse
=>
{
Some
(
LogEntry
(
s
"\n Request: $req \n Response status: ${res.status}"
,
Logging
.
InfoLevel
))
}
case
_
=>
None
// other kind of responses
}
//pathPrefixTest shields the QEP code from the redirect.
def
authenticatedInBrowser
:
Route
=
pathPrefixTest
(
"user"
|
"steward"
|
"researcher"
)
{
reportIfFailedToAuthenticate
{
authenticate
(
userAuthenticator
.
basicUserAuthenticator
)
{
user
=>
StewardDatabase
.
db
.
upsertUser
(
user
)
pathPrefix
(
"user"
)
{
userRoute
(
user
)}
~
pathPrefix
(
"steward"
)
{
stewardRoute
(
user
)}
~
pathPrefix
(
"researcher"
)
{
researcherRoute
(
user
)}
}
}
}
val
reportIfFailedToAuthenticate
=
routeRouteResponse
{
case
Rejected
(
List
(
AuthenticationFailedRejection
(
_
,
_
)))
=>
complete
(
"AuthenticationFailed"
)
}
def
makeTrouble
=
pathPrefix
(
"makeTrouble"
)
{
complete
(
throw
new
IllegalStateException
(
"fake trouble"
))
}
lazy
val
redirectToIndex
=
pathEnd
{
redirect
(
"steward/client/index.html"
,
StatusCodes
.
PermanentRedirect
)
//todo pick up "steward" programatically
}
~
(
path
(
"index.html"
)
|
pathSingleSlash
)
{
redirect
(
"client/index.html"
,
StatusCodes
.
PermanentRedirect
)
}
lazy
val
staticResources
=
pathPrefix
(
"client"
)
{
pathEnd
{
redirect
(
"client/index.html"
,
StatusCodes
.
PermanentRedirect
)
}
~
pathSingleSlash
{
redirect
(
"index.html"
,
StatusCodes
.
PermanentRedirect
)
}
~
{
getFromResourceDirectory
(
"client"
)
}
}
lazy
val
about
=
pathPrefix
(
"about"
)
{
path
(
"createTopicsMode"
)
{
get
{
complete
(
StewardConfigSource
.
createTopicsInState
.
name
)
}
}
}
def
userRoute
(
user
:
User
)
:
Route
=
get
{
pathPrefix
(
"whoami"
)
{
complete
(
OutboundUser
.
createFromUser
(
user
))
}
}
def
qepRoute
:
Route
=
pathPrefix
(
"qep"
)
{
authenticate
(
userAuthenticator
.
basicUserAuthenticator
)
{
user
=>
StewardDatabase
.
db
.
upsertUser
(
user
)
authorize
(
Authorizer
.
authorizeQep
(
user
))
{
pathPrefix
(
"requestQueryAccess"
)
(
requestQueryAccess
)
~
pathPrefix
(
"approvedTopics"
)
(
getApprovedTopicsForUser
)
}
}
}
def
requestQueryAccess
:
Route
=
post
{
requestQueryAccessWithTopic
~
requestQueryAccessWithoutTopic
}
def
requestQueryAccessWithTopic
:
Route
=
path
(
"user"
/
Segment
/
"topic"
/
IntNumber
)
{
(
userId
,
topicId
)
=>
entity
(
as
[
InboundShrineQuery
])
{
shrineQuery
:
InboundShrineQuery
=>
//todo really pull the user out of the shrine query and check vs the PM. If they aren't there, reject them for this new reason
val
result
:
(
TopicState
,
Option
[
TopicIdAndName
])
=
StewardDatabase
.
db
.
logAndCheckQuery
(
userId
,
Some
(
topicId
),
shrineQuery
)
respondWithStatus
(
result
.
_1
.
statusCode
)
{
if
(
result
.
_1
.
statusCode
==
StatusCodes
.
OK
)
complete
(
result
.
_2
.
getOrElse
(
""
))
else
complete
(
result
.
_1
.
message
)
}
}
}
def
requestQueryAccessWithoutTopic
:
Route
=
path
(
"user"
/
Segment
)
{
userId
=>
entity
(
as
[
InboundShrineQuery
])
{
shrineQuery
:
InboundShrineQuery
=>
//todo really pull the user out of the shrine query and check vs the PM. If they aren't there, reject them for this new reason
val
result
=
StewardDatabase
.
db
.
logAndCheckQuery
(
userId
,
None
,
shrineQuery
)
respondWithStatus
(
result
.
_1
.
statusCode
)
{
if
(
result
.
_1
.
statusCode
==
StatusCodes
.
OK
)
complete
(
result
.
_2
)
else
complete
(
result
.
_1
.
message
)
}
}
}
lazy
val
getApprovedTopicsForUser
:
Route
=
get
{
//todo change to "researcher"
path
(
"user"
/
Segment
)
{
userId
=>
//todo really pull the user out of the shrine query and check vs the PM. If they aren't there, reject them for this new reason
val
queryParameters
=
QueryParameters
(
researcherIdOption
=
Some
(
userId
),
stateOption
=
Some
(
TopicState
.
approved
))
val
researchersTopics
=
StewardDatabase
.
db
.
selectTopicsForResearcher
(
queryParameters
)
complete
(
researchersTopics
)
}
}
def
researcherRoute
(
user
:
User
)
:
Route
=
authorize
(
Authorizer
.
authorizeResearcher
(
user
))
{
pathPrefix
(
"topics"
)
{
getUserTopics
(
user
.
username
)
}
~
pathPrefix
(
"queryHistory"
)
{
getUserQueryHistory
(
Some
(
user
.
username
))
}
~
pathPrefix
(
"requestTopicAccess"
)
{
requestTopicAccess
(
user
)
}
~
pathPrefix
(
"editTopicRequest"
)
{
editTopicRequest
(
user
)
}
}
def
getUserTopics
(
userId
:
UserName
)
:
Route
=
get
{
//lookup topics for this user in the db
matchQueryParameters
(
Some
(
userId
)){
queryParameters
:
QueryParameters
=>
val
researchersTopics
=
StewardDatabase
.
db
.
selectTopicsForResearcher
(
queryParameters
)
complete
(
researchersTopics
)
}
}
def
matchQueryParameters
(
userName
:
Option
[
UserName
])(
parameterRoute
:
QueryParameters
=>
Route
)
:
Route
=
{
parameters
(
'state
.?,
'skip
.
as
[
Int
].?,
'limit
.
as
[
Int
].?,
'sortBy
.
as
[
String
].?,
'sortDirection
.
as
[
String
].?,
'minDate
.
as
[
Date
].?,
'maxDate
.
as
[
Date
].?)
{
(
stateStringOption
,
skipOption
,
limitOption
,
sortByOption
,
sortOption
,
minDate
,
maxDate
)
=>
val
stateTry
=
TopicState
.
stateForStringOption
(
stateStringOption
)
stateTry
match
{
case
Success
(
stateOption
)
=>
val
qp
=
QueryParameters
(
userName
,
stateOption
,
skipOption
,
limitOption
,
sortByOption
,
SortOrder
.
sortOrderForStringOption
(
sortOption
),
minDate
,
maxDate
)
parameterRoute
(
qp
)
case
Failure
(
ex
)
=>
badStateRoute
(
stateStringOption
)
}
}
}
def
badStateRoute
(
stateStringOption
:
Option
[
String
])
:
Route
=
{
respondWithStatus
(
StatusCodes
.
UnprocessableEntity
)
{
complete
(
s
"Topic state ${stateStringOption.getOrElse(s"
$stateStringOption
(
stateStringOption
should
never
be
None
at
this
point
)
")} unknown. Please specify one of ${TopicState.namesToStates.keySet}"
)
}
}
def
getUserQueryHistory
(
userIdOption
:
Option
[
UserName
])
:
Route
=
get
{
parameter
(
'asJson
.
as
[
Boolean
].?)
{
(
asJson
:
Option
[
Boolean
])
=>
path
(
"topic"
/
IntNumber
)
{
topicId
:
TopicId
=>
getQueryHistoryForUserByTopic
(
userIdOption
,
Some
(
topicId
),
asJson
)
}
~
getQueryHistoryForUserByTopic
(
userIdOption
,
None
,
asJson
)
}
}
def
getQueryHistoryForUserByTopic
(
userIdOption
:
Option
[
UserName
],
topicIdOption
:
Option
[
TopicId
],
jsonOption
:
Option
[
Boolean
]
=
None
)
=
get
{
matchQueryParameters
(
userIdOption
)
{
queryParameters
:
QueryParameters
=>
val
queryHistory
=
StewardDatabase
.
db
.
selectQueryHistory
(
queryParameters
,
topicIdOption
)
if
(
jsonOption
.
getOrElse
(
false
))
complete
(
queryHistoryWithJson
(
queryHistory
))
else
complete
(
queryHistory
)
}
}
def
queryHistoryWithJson
(
history
:
QueryHistory
)
:
QueryHistory
=
{
history
.
copy
(
queryRecords
=
history
.
queryRecords
.
map
((
record
:
OutboundShrineQuery
)
=>
{
record
.
copy
(
queryContents
=
org
.
json4s
.
native
.
Serialization
.
write
(
scala
.
xml
.
XML
.
loadString
(
record
.
queryContents
)))
}))
}
def
requestTopicAccess
(
user
:
User
)
:
Route
=
post
{
entity
(
as
[
InboundTopicRequest
])
{
topicRequest
:
InboundTopicRequest
=>
//todo notify the data stewards
StewardDatabase
.
db
.
createRequestForTopicAccess
(
user
,
topicRequest
)
complete
(
StatusCodes
.
Accepted
)
}
}
def
editTopicRequest
(
user
:
User
)
:
Route
=
post
{
path
(
IntNumber
)
{
topicId
=>
entity
(
as
[
InboundTopicRequest
])
{
topicRequest
:
InboundTopicRequest
=>
//todo notify the data stewards
val
updatedTopicTry
:
Try
[
OutboundTopic
]
=
StewardDatabase
.
db
.
updateRequestForTopicAccess
(
user
,
topicId
,
topicRequest
)
updatedTopicTry
match
{
case
Success
(
updatedTopic
)
=>
respondWithStatus
(
StatusCodes
.
Accepted
)
{
complete
(
updatedTopic
)
}
case
Failure
(
x
)
=>
x
match
{
case
x
:
TopicDoesNotExist
=>
respondWithStatus
(
StatusCodes
.
NotFound
)
{
complete
(
x
.
getMessage
)
}
case
x
:
ApprovedTopicCanNotBeChanged
=>
respondWithStatus
(
StatusCodes
.
Forbidden
)
{
complete
(
x
.
getMessage
)
}
case
x
:
DetectedAttemptByWrongUserToChangeTopic
=>
respondWithStatus
(
StatusCodes
.
Forbidden
)
{
complete
(
x
.
getMessage
)
}
case
_
=>
throw
x
}
}
}
}
}
def
stewardRoute
(
user
:
User
)
:
Route
=
authorize
(
Authorizer
.
authorizeSteward
(
user
))
{
pathPrefix
(
"queryHistory"
/
"user"
)
{
getUserQueryHistory
}
~
pathPrefix
(
"queryHistory"
)
{
getQueryHistory
}
~
pathPrefix
(
"topics"
/
"user"
)(
getUserTopicsForSteward
)
~
path
(
"topics"
){
getTopicsForSteward
}
~
pathPrefix
(
"approveTopic"
)(
approveTopicForUser
(
user
))
~
pathPrefix
(
"rejectTopic"
)(
rejectTopicForUser
(
user
))
~
pathPrefix
(
"statistics"
){
getStatistics
}
}
lazy
val
getUserQueryHistory
:
Route
=
pathPrefix
(
Segment
)
{
userId
=>
getUserQueryHistory
(
Some
(
userId
))
}
lazy
val
getQueryHistory
:
Route
=
getUserQueryHistory
(
None
)
lazy
val
getTopicsForSteward
:
Route
=
getTopicsForSteward
(
None
)
lazy
val
getUserTopicsForSteward
:
Route
=
path
(
Segment
)
{
userId
=>
getTopicsForSteward
(
Some
(
userId
))
}
def
getTopicsForSteward
(
userIdOption
:
Option
[
UserName
])
:
Route
=
get
{
//lookup topics for this user in the db
matchQueryParameters
(
userIdOption
)
{
queryParameters
:
QueryParameters
=>
val
stewardsTopics
:
StewardsTopics
=
StewardDatabase
.
db
.
selectTopicsForSteward
(
queryParameters
)
complete
(
stewardsTopics
)
}
}
def
approveTopicForUser
(
user
:
User
)
:
Route
=
changeStateForTopic
(
TopicState
.
approved
,
user
)
def
rejectTopicForUser
(
user
:
User
)
:
Route
=
changeStateForTopic
(
TopicState
.
rejected
,
user
)
def
changeStateForTopic
(
state
:
TopicState
,
user
:
User
)
:
Route
=
post
{
path
(
"topic"
/
IntNumber
)
{
topicId
=>
StewardDatabase
.
db
.
changeTopicState
(
topicId
,
state
,
user
.
username
).
fold
(
respondWithStatus
(
StatusCodes
.
UnprocessableEntity
){
complete
(
s
"No topic found for $topicId"
)
})(
topic
=>
complete
(
StatusCodes
.
OK
))
}
}
def
getStatistics
:
Route
=
pathPrefix
(
"queriesPerUser"
){
getQueriesPerUser
}
~
pathPrefix
(
"topicsPerState"
){
getTopicsPerState
}
def
getQueriesPerUser
:
Route
=
get
{
matchQueryParameters
(
None
)
{
queryParameters
:
QueryParameters
=>
val
result
=
StewardDatabase
.
db
.
selectShrineQueryCountsPerUser
(
queryParameters
)
complete
(
result
)
}
}
def
getTopicsPerState
:
Route
=
get
{
matchQueryParameters
(
None
)
{
queryParameters
:
QueryParameters
=>
val
result
=
StewardDatabase
.
db
.
selectTopicCountsPerState
(
queryParameters
)
complete
(
result
)
}
}
}
//adapted from https://gist.github.com/joseraya/176821d856b43b1cfe19
object
gruntWatchCorsSupport
extends
Directive0
with
RouteConcatenation
{
import
spray.http.HttpHeaders.
{
`Access-Control-Allow-Methods`
,
`Access-Control-Max-Age`
,
`Access-Control-Allow-Headers`
,
`Access-Control-Allow-Origin`
}
import
spray.routing.directives.RespondWithDirectives.respondWithHeaders
import
spray.routing.directives.MethodDirectives.options
import
spray.routing.directives.RouteDirectives.complete
import
spray.http.HttpMethods.
{
OPTIONS
,
GET
,
POST
}
import
spray.http.AllOrigins
private
val
allowOriginHeader
=
`Access-Control-Allow-Origin`
(
AllOrigins
)
private
val
optionsCorsHeaders
=
List
(
`Access-Control-Allow-Headers`
(
"Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, Referer, User-Agent, Authorization"
),
`Access-Control-Max-Age`
(
1728000
))
//20 days
val
gruntWatch
:
Boolean
=
StewardConfigSource
.
config
.
getBoolean
(
"shrine.steward.gruntWatch"
)
override
def
happly
(
f
:
(
HNil
)
=>
Route
)
:
Route
=
{
if
(
gruntWatch
)
{
options
{
respondWithHeaders
(
`Access-Control-Allow-Methods`
(
OPTIONS
,
GET
,
POST
)
::
allowOriginHeader
::
optionsCorsHeaders
){
complete
(
StatusCodes
.
OK
)
}
}
~
f
(
HNil
)
}
else
f
(
HNil
)
}
}
Event Timeline
Log In to Comment