Skip to content

Commit 74921b8

Browse files
Vlad BeffaVlad Beffa
authored andcommitted
SCIM server.
1 parent 0d9bc88 commit 74921b8

File tree

3 files changed

+219
-23
lines changed

3 files changed

+219
-23
lines changed

app/controllers/SCIMController.scala

Lines changed: 210 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,245 @@
11
package controllers
22

33
import javax.inject._
4+
import java.sql.ResultSet
5+
import com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException
6+
47
import play.api.db.Database
58
import play.api.mvc._
9+
import play.api.libs.json._
610

11+
// import scala.concurrent.ExecutionContext.Implicits.global
712

813
class SCIMController @Inject() (db:Database) extends Controller {
914

10-
def users(filter:Option[String], count:Option[String], startIndex:Option[String]): Result = {
15+
// ----------- Users ----------- //
16+
17+
def users(filter:Option[String], count:Option[String], startIndex:Option[String]) = Action {
1118
// TODO: Retrieve paginated User Objects
1219
// TODO: Allow for an equals and startsWith filters on username
13-
Ok
20+
val query = "SELECT * FROM users" +
21+
filter.map(" WHERE username LIKE '" + _ + "%'").getOrElse("") +
22+
count.map(" LIMIT " + _).getOrElse("") +
23+
startIndex.map(" OFFSET " + _).getOrElse("")
24+
25+
db.withConnection { conn =>
26+
val stmt = conn.createStatement
27+
val rs = stmt.executeQuery(query)
28+
val users: Stream[JsValue] = results(rs)(parseUser)
29+
30+
Ok(Json.obj("users" -> users))
31+
}
1432
}
1533

16-
def user(uid:String): Result = {
34+
def user(uid:String) = Action {
1735
// TODO: Retrieve a single User Object by ID
18-
Ok
36+
val query = s"SELECT * FROM users WHERE id = '${uid}'"
37+
38+
db.withConnection { conn =>
39+
val stmt = conn.createStatement
40+
val rs = stmt.executeQuery(query)
41+
42+
if (rs.next())
43+
Ok(parseUser(rs))
44+
else
45+
NotFound
46+
}
1947
}
2048

21-
def createUser(): Result = {
49+
def createUser() = Action { implicit request: Request[AnyContent] =>
2250
// TODO: Create a User Object with firstname and lastname metadata
23-
Ok
51+
val id = field[String]("id")
52+
val username = field[String]("username")
53+
val firstname = field[String]("firstname")
54+
val lastname = field[String]("lastname")
55+
val active = field[Boolean]("active", false)
56+
57+
try {
58+
doCreateUser(id, username, firstname, lastname, active)
59+
} catch {
60+
case dup: MySQLIntegrityConstraintViolationException => BadRequest(dup.getMessage)
61+
case _: Throwable => InternalServerError
62+
}
2463
}
2564

26-
def updateUser(uid:String): Result = {
65+
def updateUser(uid:String) = Action { implicit request: Request[AnyContent] =>
2766
// TODO: Update a User Object's firstname, lastname, and active status
28-
Ok
67+
val firstname = fieldOpt[String]("firstname")
68+
val lastname = fieldOpt[String]("lastname")
69+
val active = fieldOpt[Boolean]("active")
70+
71+
doUpdateUser(Some(uid), firstname, lastname, active)
2972
}
3073

31-
def deleteUser(uid:String): Result = {
74+
def deleteUser(uid:String) = Action {
3275
// TODO: Delete a User Object by ID
33-
Ok
76+
doDeleteUser(uid)
3477
}
3578

36-
def groups(count:Option[String], startIndex:Option[String]): Result = {
79+
// ----------- Groups ----------- //
80+
81+
def groups(count:Option[String], startIndex:Option[String]) = Action {
3782
// TODO: Retrieve paginated Group Objects
38-
Ok
83+
val query = "SELECT * FROM groups" +
84+
count.map(" LIMIT " + _).getOrElse("") +
85+
startIndex.map(" OFFSET " + _).getOrElse("")
86+
87+
db.withConnection { conn =>
88+
val stmt = conn.createStatement
89+
val rs = stmt.executeQuery(query)
90+
val groups: Stream[JsValue] = results(rs)(parseGroup)
91+
92+
Ok(Json.obj("groups" -> groups))
93+
}
3994
}
4095

41-
def group(groupId:String): Result = {
96+
def group(groupId:String) = Action {
4297
// TODO: Retrieve a single Group Object by ID
43-
Ok
98+
val query = s"""
99+
SELECT users.* FROM users
100+
INNER JOIN users_groups
101+
ON users.id = users_groups.user_id
102+
WHERE group_id = '${groupId}'"""
103+
104+
db.withConnection { conn =>
105+
val stmt = conn.createStatement
106+
val rs = stmt.executeQuery(query)
107+
val users: Stream[JsValue] = results(rs)(parseUser)
108+
109+
Ok(Json.obj(
110+
"id" -> groupId,
111+
"users" -> users))
112+
}
44113
}
45114

46-
def patchGroup(groupId:String): Result = {
115+
def patchGroup(groupId:String) = Action { implicit request: Request[AnyContent] =>
47116
// TODO: Patch a Group Object, modifying its members
48-
Ok
117+
val add = fieldOpt[List[JsValue]]("add")
118+
val delete = fieldOpt[List[String]]("delete")
119+
val update = fieldOpt[List[JsValue]]("update")
120+
121+
if (add.isEmpty && delete.isEmpty && update.isEmpty) {
122+
BadRequest("empty patch request")
123+
} else {
124+
// this should probably be in a transaction to roll back in case of an exception
125+
try {
126+
add.map(_.foreach(user => (doCreateUser _).tupled(toUserTuple(user)))) // this ignores the result of calls to doCreateUser
127+
delete.map(_.foreach(doDeleteUser)) // this ignores the result of calls to doDeleteUser
128+
update.map(_.foreach(user => (doUpdateUser _).tupled(toUserOptTuple(user)))) // this ignores the result of calls to doUpdateUser
129+
Ok
130+
} catch {
131+
case dup: MySQLIntegrityConstraintViolationException => BadRequest(dup.getMessage)
132+
case _: Throwable => InternalServerError
133+
}
134+
}
135+
}
136+
137+
// ----------- Utilities ----------- //
138+
139+
// taken from https://stackoverflow.com/questions/9636545/treating-an-sql-resultset-like-a-scala-stream
140+
private def results[T](resultSet: ResultSet)(f: ResultSet => T): Stream[T] = {
141+
new Iterator[T] {
142+
def hasNext = resultSet.next()
143+
def next() = f(resultSet)
144+
}.toStream
49145
}
50146

147+
private def parseUser(rs: ResultSet): JsValue =
148+
Json.obj(
149+
"id" -> rs.getString("id"),
150+
"username" -> rs.getString("username"),
151+
"firstname" -> rs.getString("firstname"),
152+
"lastname" -> rs.getString("lastname"),
153+
"active" -> (if (rs.getInt("active") == 1) "true" else "false")
154+
)
155+
156+
private def parseGroup(rs: ResultSet): JsValue =
157+
Json.obj(
158+
"id" -> rs.getString("id")
159+
)
160+
161+
private def toUserTuple(user: JsValue) =
162+
(
163+
(user \ "id").asOpt[String].getOrElse(null),
164+
(user \ "username").asOpt[String].getOrElse(null),
165+
(user \ "firstname").asOpt[String].getOrElse(null),
166+
(user \ "lastname").asOpt[String].getOrElse(null),
167+
(user \ "active").asOpt[Boolean].getOrElse(false))
168+
169+
private def toUserOptTuple(user: JsValue) =
170+
(
171+
(user \ "id").asOpt[String],
172+
(user \ "firstname").asOpt[String],
173+
(user \ "lastname").asOpt[String],
174+
(user \ "active").asOpt[Boolean])
175+
176+
private def doCreateUser(id: String, username: String, firstname: String, lastname: String, active: Boolean) = {
177+
if (id == null) {
178+
BadRequest("missing id")
179+
} else if (username == null) {
180+
BadRequest("missing username")
181+
} else if (firstname == null) {
182+
BadRequest("missing firstname")
183+
} else if (lastname == null) {
184+
BadRequest("missing lastname")
185+
} else {
186+
val query = s"""
187+
INSERT INTO users (id, username, firstname, lastname, active)
188+
VALUES
189+
('${id}', '${username}', '${firstname}', '${lastname}', ${active});
190+
"""
191+
192+
db.withConnection { conn =>
193+
val stmt = conn.createStatement
194+
val rc = stmt.executeUpdate(query)
195+
196+
if (rc == 1)
197+
Ok
198+
else
199+
InternalServerError("failed to create user")
200+
}
201+
}
202+
}
203+
204+
private def doDeleteUser(id: String) = {
205+
val query = s"DELETE FROM users WHERE id = '${id}'"
206+
207+
db.withConnection { conn =>
208+
val stmt = conn.createStatement
209+
val rc = stmt.executeUpdate(query)
210+
211+
if (rc == 1)
212+
Ok
213+
else
214+
NotFound
215+
}
216+
}
217+
218+
private def doUpdateUser(id: Option[String], firstname: Option[String], lastname: Option[String], active: Option[Boolean]) = {
219+
if (id.isEmpty) {
220+
BadRequest("missing id")
221+
} else if (firstname.isEmpty && lastname.isEmpty && active.isEmpty) {
222+
BadRequest("must pass at least one of firstname, lastname, active to update")
223+
} else {
224+
val query = "UPDATE users SET" +
225+
(firstname.map(" firstname = '" + _ + "',").getOrElse("") +
226+
lastname.map(" lastname = '" + _ + "',").getOrElse("") +
227+
active.map(" active = " + _ + ",").getOrElse("")).dropRight(1) + // dropRight removes extra trailing comma
228+
s" WHERE id = '${id.get}'"
229+
230+
db.withConnection { conn =>
231+
val stmt = conn.createStatement
232+
val rc = stmt.executeUpdate(query)
233+
234+
if (rc == 1)
235+
Ok("user updated")
236+
else
237+
NotFound
238+
}
239+
}
240+
}
241+
242+
private def field[T: Reads](field: String, default: T = null)(implicit request: Request[AnyContent]) = fieldOpt[T](field).getOrElse(default)
243+
private def fieldOpt[T: Reads](field: String)(implicit request: Request[AnyContent]) = request.body.asJson.map(_ \ field).flatMap(_.asOpt[T])
244+
51245
}

build.sbt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ scalaVersion := "2.11.7"
88

99
libraryDependencies ++= Seq(
1010
javaJdbc,
11+
evolutions,
12+
jdbc,
1113
"com.typesafe.play" %% "anorm" % "2.5.0",
1214
"mysql" % "mysql-connector-java" % "5.1.23",
1315
cache,
@@ -16,5 +18,4 @@ libraryDependencies ++= Seq(
1618
)
1719

1820

19-
20-
fork in run := true
21+
fork in run := false

conf/application.conf

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -341,11 +341,12 @@ db {
341341
# You can declare as many datasources as you want.
342342
# By convention, the default datasource is named `default`
343343

344-
# https://www.playframework.com/documentation/latest/Developing-with-the-H2-Database
345-
#default.driver = org.h2.Driver
346-
#default.url = "jdbc:h2:mem:play"
347-
#default.username = sa
348-
#default.password = ""
344+
default {
345+
driver=com.mysql.jdbc.Driver
346+
url="jdbc:mysql://localhost/scim"
347+
username=scim
348+
password="scim"
349+
}
349350

350351
# You can turn on SQL logging for any datasource
351352
# https://www.playframework.com/documentation/latest/Highlights25#Logging-SQL-statements

0 commit comments

Comments
 (0)