-
Notifications
You must be signed in to change notification settings - Fork 64
/
EssentialActions.scala
204 lines (178 loc) · 7.05 KB
/
EssentialActions.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
package controllers
import scala.concurrent.Future
// Import the method statically and rename it
import scala.concurrent.Future.{successful => resolve}
import scala.concurrent.ExecutionContext.Implicits.global
import play.api._
import play.api.mvc._
import play.api.libs.iteratee.{Iteratee,Done}
import play.api.libs.json._
import models._
import models.json._
/*
* 1. EssentialActions
* ~~~~
* EssentialActions replace Play 2.0/2.1's Action composition.
* Put your 'high-level' actions in a trait that can be mixed into every controller. Stuff like
* authentication and authorization will be used by every controller throughout your app.
* EssentialActions allow you to easily compose functions that depend on the request but ignore
* the body.
* However check first if you can solve the problem at the EssentialFilter level.
*/
object EssentialActions extends Controller {
/* Let's start with a simple example where you just wrap any other action, be it essential or not */
/** Prints the time elapsed for a wrapped action to execute */
def TimeElapsed(action: EssentialAction): EssentialAction = EssentialAction { requestHeader =>
val start = System.currentTimeMillis
action(requestHeader).map { res =>
val totalTime = System.currentTimeMillis - start
println("Elapsed time: %1d ms".format(totalTime))
res
}
}
def short = TimeElapsed {
Action {
val res = for (i <- 0 until 100000) yield i
Ok(res.mkString(", "))
}
}
def shortAsync = TimeElapsed {
Action.async {
Future {
val res = for (i <- 0 until 100000) yield i
Ok(res.mkString(", "))
}
}
}
/*
* Next up is authorization handling. This is such a good use-case for EssentialActions because
* not only is the body completely uninteresting if the user is not authenticated or authorized,
* but also because EssentialActions are just functions, and so we can build one on top of the
* other.
*/
/**
* Reads the security token from the RequestHeader. If the header is correct, the enclosed Action
* will be executed. Otherwise returns a 401 Unauthorized response without invoking the action.
* We not only wrap an action but pass the extracted token to that action.
*/
def HasToken(action: String => EssentialAction): EssentialAction = EssentialAction { requestHeader =>
val maybeToken = requestHeader.headers.get("X-SECRET-TOKEN")
maybeToken map { token =>
action(token)(requestHeader) // apply requestHeader to EssentialAction produces the Iteratee[Array[Byte], SimpleResult]
} getOrElse {
Done(Unauthorized("401 No Security Token\n")) // 'Done' means the Iteratee has completed its computations
}
}
/**
* HasPermission directly builds on top of HasToken. If there is no token, this action will never
* be executed. Implementing this in an OO-style proves to be cumbersome.
* An example for an OO-EssentialAction is [[play.api.cache.Cached]].
*/
def HasPermission(permissions: Permission*)(action: User => EssentialAction): EssentialAction =
HasToken { token => // We compose with HasToken to make sure there is a valid token, and then use the token
EssentialAction { requestHeader => // EssentialAction again, because we need the header and we must return an EA
val user = User.getByToken(token) // Use the token to the retrieve the User
user map { user =>
if (permissions.contains(user.permission)) { // The actual permissions check
action(user)(requestHeader) // Execute only if permissions match
} else {
Done[Array[Byte], SimpleResult](Forbidden) // Must be typed because the compiler cannot infer
}
} getOrElse(Done[Array[Byte], SimpleResult](NotFound)) // Must be typed because the compiler cannot infer
}
}
/*
* Should pass: curl -H "X-SECRET-TOKEN: secret-123" localhost:9000/ea/token
* Should fail: curl localhost:9000/ea/token
*/
def withToken = HasToken { token =>
Action { request =>
Ok("Access granted.\n")
}
}
/*
* Should pass: curl -H "X-SECRET-TOKEN: secret-123" localhost:9000/ea/token-async
* Should fail: curl localhost:9000/ea/token-async
*/
def withTokenLongRunning = HasToken { token =>
Action.async {
Future.successful(Ok("Access granted.\n"))
}
}
def adminAction = HasPermission(AdminPermission) { user =>
Action { request =>
Ok("Admin Access granted.\n")
}
}
def adminActionLong = HasPermission(AdminPermission) { user =>
Action.async { request =>
Future.successful(Ok("Admin Access granted.\n"))
}
}
// Compose on the spot; this action reduces boilerplate
def CanEditUser(id: Long)(action: User => EssentialAction): EssentialAction = HasPermission(UserPermission) { currentUser =>
EssentialAction { requestHeader =>
if (currentUser.id.exists(_ == id)) {
action(currentUser)(requestHeader)
} else {
Done(Forbidden("403 Not allowed to access this user.\n"))
}
}
}
/** An action without body, should use the empty body parser to avoid problems with Content-Type. */
def fetchUser(id: Long) = CanEditUser(id) { user =>
// If we specify a body parser, Action must have a parameter (Request[A]), which we choose to ignore
// `parse` can be found in [[play.api.mvc.BodyParsers]]
Action(parse.empty) { _ => // The underscore signals to the reader that the parameter isn't used
Ok
}
}
def updateUser(id: Long) = CanEditUser(id) { user =>
Action(parse.json) { request =>
request.body.validate[User] match {
case JsSuccess(user, _) => {
// User.update(user)
Ok
}
case JsError(err) => BadRequest
}
}
}
/** An action without body, should use the empty body parser to avoid problems with Content-Type. */
def delete(id: Long) = HasPermission(UserPermission) { _ =>
Action(parse.empty) { _ =>
Ok(s"Deleted $id\n")
}
}
/**
* We could also use EssentialAction directly, but it's a little too ugly.
* This approach is not recommended.
*/
def deleteWithEssentials(id: Long) = HasPermission(UserPermission) { _ =>
EssentialAction { _ =>
Done(Ok(s"Deleted $id\n"))
}
}
// Inventory - another example of nesting EssentialActions and applying business rules
def CanAccessInventory(id: Long)(action: User => Inventory => EssentialAction): EssentialAction =
HasPermission(AdminPermission) { user =>
EssentialAction { requestHeader =>
val maybeInventory = Inventory.findOne()
maybeInventory.map { inventory =>
// Business rule: Inventory must be associated with matching department
if (user.department == inventory.department) {
action(user)(inventory)(requestHeader)
} else {
Done[Array[Byte], SimpleResult](Forbidden)
}
}.getOrElse(Done[Array[Byte], SimpleResult](NotFound))
}
}
def getInventory(id: Long) = CanAccessInventory(id) { user => inventory =>
TimeElapsed {
Action(parse.empty) { request =>
Ok
}
}
}
}