Skip to content

Commit ae5c90f

Browse files
authored
Support private extractor registration (#300)
* add private registration endpoint * add user filter * unique key + perms * make extractor key an option * list user extractors if no process type specified * Permission check updates
1 parent 32da80b commit ae5c90f

File tree

11 files changed

+288
-166
lines changed

11 files changed

+288
-166
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## Unreleased
99

10+
### Added
11+
- Add "when" parameter in a few GET API endpoints to enable pagination [#266](https://github.com/clowder-framework/clowder/issues/266)
12+
- Extractors can now specify an extractor_key and an owner (email address) when sending a
13+
registration or heartbeat to Clowder that will restrict use of that extractor to them.
14+
1015
## Fixed
1116
- Updated lastModifiesDate when updating file or metadata to a dataset, added lastModified to UI [386](https://github.com/clowder-framework/clowder/issues/386)
1217
- Disabled button while create dataset ajax call is still going on [#311](https://github.com/clowder-framework/clowder/issues/311)

app/api/Extractions.scala

Lines changed: 66 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -127,36 +127,6 @@ class Extractions @Inject()(
127127
}
128128
}
129129

130-
/**
131-
*
132-
* Given a file id (UUID), submit this file for extraction
133-
*/
134-
def submitExtraction(id: UUID) = PermissionAction(Permission.ViewFile, Some(ResourceRef(ResourceRef.file, id)))(parse.json) { implicit request =>
135-
if (UUID.isValid(id.stringify)) {
136-
files.get(id) match {
137-
case Some(file) => {
138-
// FIXME dataset not available?
139-
routing.fileCreated(file, None, Utils.baseUrl(request).toString, request.apiKey) match {
140-
case Some(jobId) => {
141-
Ok(Json.obj("status" -> "OK", "job_id" -> jobId))
142-
}
143-
case None => {
144-
val message = "No jobId found for Extraction"
145-
Logger.error(message)
146-
InternalServerError(toJson(Map("status" -> "KO", "message" -> message)))
147-
}
148-
}
149-
}
150-
case None => {
151-
Logger.error("Could not retrieve file that was just saved.")
152-
InternalServerError("Error uploading file")
153-
}
154-
} //file match
155-
} else {
156-
BadRequest("Not valid id")
157-
}
158-
}
159-
160130
/**
161131
* For a given file id, checks for the status of all extractors processing that file.
162132
* REST endpoint GET /api/extractions/:id/status
@@ -404,24 +374,24 @@ class Extractions @Inject()(
404374
Ok(jarr)
405375
}
406376

407-
def listExtractors(categories: List[String]) = AuthenticatedAction { implicit request =>
408-
Ok(Json.toJson(extractors.listExtractorsInfo(categories)))
377+
def listExtractors(categories: List[String], space: Option[UUID]) = AuthenticatedAction { implicit request =>
378+
val userid = request.user.map(u => Some(u.id)).getOrElse(None)
379+
Ok(Json.toJson(extractors.listExtractorsInfo(categories, userid)))
409380
}
410381

411-
def getExtractorInfo(extractorName: String) = AuthenticatedAction { implicit request =>
412-
extractors.getExtractorInfo(extractorName) match {
382+
def getExtractorInfo(extractorName: String, extractor_key: Option[String]) = AuthenticatedAction { implicit request =>
383+
extractors.getExtractorInfo(extractorName, extractor_key, request.user) match {
413384
case Some(info) => Ok(Json.toJson(info))
414385
case None => NotFound(Json.obj("status" -> "KO", "message" -> "Extractor info not found"))
415386
}
416387
}
417388

418-
def deleteExtractor(extractorName: String) = ServerAdminAction { implicit request =>
419-
extractors.deleteExtractor(extractorName)
389+
def deleteExtractor(extractorName: String, extractor_key: Option[String]) = ServerAdminAction { implicit request =>
390+
extractors.deleteExtractor(extractorName, extractor_key)
420391
Ok(toJson(Map("status" -> "success")))
421392
}
422393

423-
def addExtractorInfo() = AuthenticatedAction(parse.json) { implicit request =>
424-
394+
def addExtractorInfo(extractor_key: Option[String], user: Option[String]) = AuthenticatedAction(parse.json) { implicit request =>
425395
// If repository is of type object, change it into an array.
426396
// This is for backward compatibility with requests from existing extractors.
427397
var requestJson = request.body \ "repository" match {
@@ -438,34 +408,66 @@ class Extractions @Inject()(
438408
BadRequest(Json.obj("status" -> "KO", "message" -> JsError.toFlatJson(errors)))
439409
},
440410
info => {
441-
extractors.updateExtractorInfo(info) match {
442-
case Some(u) => {
443-
// Create/assign any default labels for this extractor
444-
u.defaultLabels.foreach(labelStr => {
445-
val segments = labelStr.split("/")
446-
val (labelName, labelCategory) = if (segments.length > 1) {
447-
(segments(1), segments(0))
448-
} else {
449-
(segments(0), "Other")
411+
// Check private extractor flags
412+
val submissionInfo: Option[ExtractorInfo] = extractor_key match {
413+
case Some(ek) => {
414+
user match {
415+
case None => {
416+
Logger.error("Extractors with a private key must also specify a user email.")
417+
None
450418
}
451-
extractors.getExtractorsLabel(labelName) match {
452-
case None => {
453-
// Label does not exist - create and assign it
454-
val createdLabel = extractors.createExtractorsLabel(labelName, Some(labelCategory), List[String](u.name))
455-
}
456-
case Some(lbl) => {
457-
// Label already exists, assign it
458-
if (!lbl.extractors.contains(u.name)) {
459-
val label = ExtractorsLabel(lbl.id, lbl.name, lbl.category, lbl.extractors ++ List[String](u.name))
460-
val updatedLabel = extractors.updateExtractorsLabel(label)
419+
case Some(userEmail) => {
420+
userservice.findByEmail(userEmail) match {
421+
case Some(u) => {
422+
val perms = List(new ResourceRef('user, u.id))
423+
Some(info.copy(unique_key=Some(ek), permissions=perms))
424+
}
425+
case None => {
426+
Logger.error("No user found with email "+userEmail)
427+
None
461428
}
462429
}
463430
}
464-
})
431+
}
432+
}
433+
case None => Some(info)
434+
}
435+
436+
// TODO: Check user permissions if the extractor_key has already been registered
437+
438+
submissionInfo match {
439+
case None => BadRequest("Extractors with a private key must also specify a non-anonymous user.")
440+
case Some(subInfo) => {
441+
extractors.updateExtractorInfo(subInfo) match {
442+
case Some(u) => {
443+
// Create/assign any default labels for this extractor
444+
u.defaultLabels.foreach(labelStr => {
445+
val segments = labelStr.split("/")
446+
val (labelName, labelCategory) = if (segments.length > 1) {
447+
(segments(1), segments(0))
448+
} else {
449+
(segments(0), "Other")
450+
}
451+
extractors.getExtractorsLabel(labelName) match {
452+
case None => {
453+
// Label does not exist - create and assign it
454+
val createdLabel = extractors.createExtractorsLabel(labelName, Some(labelCategory), List[String](u.name))
455+
}
456+
case Some(lbl) => {
457+
// Label already exists, assign it
458+
if (!lbl.extractors.contains(u.name)) {
459+
val label = ExtractorsLabel(lbl.id, lbl.name, lbl.category, lbl.extractors ++ List[String](u.name))
460+
val updatedLabel = extractors.updateExtractorsLabel(label)
461+
}
462+
}
463+
}
464+
})
465465

466-
Ok(Json.obj("status" -> "OK", "message" -> ("Extractor info updated. ID = " + u.id)))
466+
Ok(Json.obj("status" -> "OK", "message" -> ("Extractor info updated. ID = " + u.id)))
467+
}
468+
case None => BadRequest(Json.obj("status" -> "KO", "message" -> "Error updating extractor info"))
469+
}
467470
}
468-
case None => BadRequest(Json.obj("status" -> "KO", "message" -> "Error updating extractor info"))
469471
}
470472
}
471473
)
@@ -518,11 +520,14 @@ class Extractions @Inject()(
518520
}
519521
// if extractor_id is not specified default to execution of all extractors matching mime type
520522
(request.body \ "extractor").asOpt[String] match {
521-
case Some(extractorId) =>
523+
case Some(extractorId) => {
524+
val extractorKey = (request.body \ "extractor").asOpt[String]
525+
extractors.getExtractorInfo(extractorId, extractorKey, request.user)
522526
val job_id = routing.submitFileManually(new UUID(originalId), file, Utils.baseUrl(request), extractorId, extra,
523527
datasetId, newFlags, request.apiKey, request.user)
524528
sink.logSubmitFileToExtractorEvent(file, extractorId, request.user)
525529
Ok(Json.obj("status" -> "OK", "job_id" -> job_id))
530+
}
526531
case None => {
527532
routing.fileCreated(file, None, Utils.baseUrl(request).toString, request.apiKey) match {
528533
case Some(job_id) => {

app/controllers/Extractors.scala

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ class Extractors @Inject() (extractions: ExtractionService,
3939
/**
4040
* Gets a map of all updates from all jobs given to this extractor.
4141
*/
42-
def showJobHistory(extractorName: String) = AuthenticatedAction { implicit request =>
42+
def showJobHistory(extractorName: String, extractor_key: Option[String]) = AuthenticatedAction { implicit request =>
4343
implicit val user = request.user
44-
extractorService.getExtractorInfo(extractorName) match {
44+
extractorService.getExtractorInfo(extractorName, extractor_key, user) match {
4545
case None => NotFound(s"No extractor found with name=${extractorName}")
4646
case Some(info) => {
4747
val allExtractions = extractions.findAll()
@@ -56,9 +56,10 @@ class Extractors @Inject() (extractions: ExtractionService,
5656
*/
5757
def selectExtractors() = AuthenticatedAction { implicit request =>
5858
implicit val user = request.user
59-
59+
val userid = request.user.map(u => Some(u.id)).getOrElse(None)
6060
// Filter extractors by user filters necessary
61-
var runningExtractors: List[ExtractorInfo] = extractorService.listExtractorsInfo(List.empty)
61+
// TODO: Filter by multiple spaces
62+
var runningExtractors: List[ExtractorInfo] = extractorService.listExtractorsInfo(List.empty, userid)
6263
val selectedExtractors: List[String] = extractorService.getEnabledExtractors()
6364
val groups = extractions.groupByType(extractions.findAll())
6465
val allLabels = extractorService.listExtractorsLabels()
@@ -166,7 +167,7 @@ class Extractors @Inject() (extractions: ExtractionService,
166167
def manageLabels = ServerAdminAction { implicit request =>
167168
implicit val user = request.user
168169
val categories = List[String]("EXTRACT")
169-
val extractors = extractorService.listExtractorsInfo(categories)
170+
val extractors = extractorService.listExtractorsInfo(categories, None)
170171
val labels = extractorService.listExtractorsLabels()
171172

172173
Ok(views.html.extractorLabels(labels, extractors))
@@ -211,7 +212,8 @@ class Extractors @Inject() (extractions: ExtractionService,
211212

212213
def showExtractorInfo(extractorName: String) = AuthenticatedAction { implicit request =>
213214
implicit val user = request.user
214-
val targetExtractor = extractorService.listExtractorsInfo(List.empty).find(p => p.name == extractorName)
215+
val userid = request.user.map(u => Some(u.id)).getOrElse(None)
216+
val targetExtractor = extractorService.listExtractorsInfo(List.empty, userid).find(p => p.name == extractorName)
215217
targetExtractor match {
216218
case Some(extractor) => {
217219
val labels = extractorService.getLabelsForExtractor(extractor.name)
@@ -223,6 +225,7 @@ class Extractors @Inject() (extractions: ExtractionService,
223225

224226
def showExtractorMetrics(extractorName: String) = AuthenticatedAction { implicit request =>
225227
implicit val user = request.user
228+
val userid = request.user.map(u => Some(u.id)).getOrElse(None)
226229

227230
val dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS")
228231
val todaydate = dateFormatter.format(new java.util.Date())
@@ -299,7 +302,7 @@ class Extractors @Inject() (extractions: ExtractionService,
299302
}
300303
Logger.warn("last 10 average: " + lastTenAverage)
301304

302-
val targetExtractor = extractorService.listExtractorsInfo(List.empty).find(p => p.name == extractorName)
305+
val targetExtractor = extractorService.listExtractorsInfo(List.empty, userid).find(p => p.name == extractorName)
303306
targetExtractor match {
304307
case Some(extractor) => Ok(views.html.extractorMetrics(extractorName, average.toString, lastTenAverage.toString, lastweeksubmitted, lastmonthsubmitted))
305308
case None => InternalServerError("Extractor Info not found: " + extractorName)
@@ -308,11 +311,19 @@ class Extractors @Inject() (extractions: ExtractionService,
308311

309312
def submitFileExtraction(file_id: UUID) = PermissionAction(Permission.EditFile, Some(ResourceRef(ResourceRef.file, file_id))) { implicit request =>
310313
implicit val user = request.user
311-
val all_extractors = extractorService.listExtractorsInfo(List("EXTRACT", "CONVERT"))
312-
val extractors = all_extractors.filter(!_.process.file.isEmpty)
314+
val userid = request.user.map(u => Some(u.id)).getOrElse(None)
313315
fileService.get(file_id) match {
314-
315316
case Some(file) => {
317+
val all_extractors = extractorService.listExtractorsInfo(List("EXTRACT", "CONVERT"), userid)
318+
var extractors = all_extractors.filter(!_.process.file.isEmpty)
319+
320+
val user_extra = userid match {
321+
case Some(uid) => all_extractors.filter(_.permissions.contains(ResourceRef('user, uid)))
322+
case None => List.empty
323+
}
324+
325+
extractors = (extractors ++ user_extra).distinct
326+
316327
val foldersContainingFile = folders.findByFileId(file.id).sortBy(_.name)
317328
var folderHierarchy = new ListBuffer[Folder]()
318329
if(foldersContainingFile.length > 0) {
@@ -352,7 +363,8 @@ class Extractors @Inject() (extractions: ExtractionService,
352363

353364
def submitSelectedExtractions(ds_id: UUID) = PermissionAction(Permission.EditDataset, Some(ResourceRef(ResourceRef.dataset, ds_id))) { implicit request =>
354365
implicit val user = request.user
355-
val all_extractors = extractorService.listExtractorsInfo(List("EXTRACT", "CONVERT"))
366+
val userid = request.user.map(u => Some(u.id)).getOrElse(None)
367+
val all_extractors = extractorService.listExtractorsInfo(List("EXTRACT", "CONVERT"), userid)
356368
val extractors = all_extractors.filter(!_.process.file.isEmpty)
357369
datasets.get(ds_id) match {
358370
case Some(dataset) => {
@@ -372,10 +384,13 @@ class Extractors @Inject() (extractions: ExtractionService,
372384

373385
def submitDatasetExtraction(ds_id: UUID) = PermissionAction(Permission.EditDataset, Some(ResourceRef(ResourceRef.dataset, ds_id))) { implicit request =>
374386
implicit val user = request.user
375-
val all_extractors = extractorService.listExtractorsInfo(List("EXTRACT", "CONVERT"))
376-
val extractors = all_extractors.filter(!_.process.dataset.isEmpty)
387+
val userid = request.user.map(u => Some(u.id)).getOrElse(None)
377388
datasetService.get(ds_id) match {
378-
case Some(ds) => Ok(views.html.extractions.submitDatasetExtraction(extractors, ds))
389+
case Some(ds) => {
390+
val all_extractors = extractorService.listExtractorsInfo(List("EXTRACT", "CONVERT"), userid)
391+
val extractors = all_extractors.filter(!_.process.dataset.isEmpty)
392+
Ok(views.html.extractions.submitDatasetExtraction(extractors, ds))
393+
}
379394
case None => InternalServerError("Dataset not found")
380395
}
381396
}

app/controllers/Spaces.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ class Spaces @Inject() (spaces: SpaceService, users: UserService, events: EventS
8383
def selectExtractors(id: UUID) = AuthenticatedAction {
8484
implicit request =>
8585
implicit val user = request.user
86+
val userid = request.user.map(u => Some(u.id)).getOrElse(None)
8687
spaces.get(id) match {
8788
case Some(s) => {
8889
// get list of registered extractors
89-
val runningExtractors: List[ExtractorInfo] = extractors.listExtractorsInfo(List.empty)
90+
val runningExtractors: List[ExtractorInfo] = extractors.listExtractorsInfo(List.empty, userid)
9091
// list of extractors enabled globally
9192
val globalSelections: List[String] = extractors.getEnabledExtractors()
9293
// get list of extractors registered with a specific space

app/models/Extraction.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ case class ExtractorDetail(
7878
*
7979
* @param id id internal to the system
8080
* @param name lower case, no spaces, can use dashes
81+
* @param uniqueName name+suffix to uniquely identify extractor for private use e.g. clowder.extractor.v2.johndoe123
8182
* @param version the version, for example 1.3.5
8283
* @param updated date when this information was last updated
8384
* @param description short description of what the extractor does
@@ -117,7 +118,9 @@ case class ExtractorInfo(
117118
defaultLabels: List[String] = List[String](),
118119
process: ExtractorProcessTriggers = new ExtractorProcessTriggers(),
119120
categories: List[String] = List[String](ExtractorCategory.EXTRACT.toString),
120-
parameters: JsValue = JsObject(Seq())
121+
parameters: JsValue = JsObject(Seq()),
122+
unique_key: Option[String] = None,
123+
permissions: List[ResourceRef] =List[ResourceRef]()
121124
)
122125

123126
/** what are the categories of the extractor?
@@ -170,7 +173,9 @@ object ExtractorInfo {
170173
(JsPath \ "labels").read[List[String]].orElse(Reads.pure(List.empty)) and
171174
(JsPath \ "process").read[ExtractorProcessTriggers].orElse(Reads.pure(new ExtractorProcessTriggers())) and
172175
(JsPath \ "categories").read[List[String]].orElse(Reads.pure(List[String](ExtractorCategory.EXTRACT.toString))) and
173-
(JsPath \ "parameters").read[JsValue].orElse(Reads.pure(JsObject(Seq())))
176+
(JsPath \ "parameters").read[JsValue].orElse(Reads.pure(JsObject(Seq()))) and
177+
(JsPath \ "unique_key").read[Option[String]].orElse(Reads.pure(None)) and
178+
(JsPath \ "permissions").read[List[ResourceRef]].orElse(Reads.pure(List.empty))
174179
)(ExtractorInfo.apply _)
175180
}
176181

0 commit comments

Comments
 (0)