tanishqgandhi1908 commented on code in PR #7937:
URL: https://github.com/apache/texera/pull/7937#discussion_r3874770882


##########
file-service/src/main/scala/org/apache/texera/service/resource/ModelResource.scala:
##########
@@ -911,10 +1015,114 @@ class ModelResource extends LazyLogging {
     )
   }
 
+  // 
===========================================================================
+  // Cover image
+  // 
===========================================================================
+
+  /** Points the model card at a committed image inside the model, 
"<version>/<file>". */
+  @POST
+  @RolesAllowed(Array("REGULAR", "ADMIN"))
+  @Path("/{mid}/update/cover")
+  @Consumes(Array(MediaType.APPLICATION_JSON))
+  def updateModelCoverImage(
+      @PathParam("mid") mid: Integer,
+      request: CoverImageRequest,
+      @Auth sessionUser: SessionUser
+  ): Response = {
+    withTransaction(context) { ctx =>
+      val model = getModelByID(ctx, mid)
+      if (!userHasWriteAccess(ctx, mid, sessionUser.getUid)) {
+        throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
+      }
+
+      val normalized =
+        CoverImageUtils.validatePathOrThrow(request.coverImage, 
CoverImageUtils.MAX_PATH_LENGTH)
+
+      val document = CoverImageUtils.openCoverOrBadRequest(
+        ResourceType.Model,
+        getOwner(ctx, mid).getEmail,
+        model.getName,
+        normalized
+      )
+      
CoverImageUtils.requireWithinSizeLimit(CoverImageUtils.fileSizeOf(document, 
normalized))
+
+      model.setCoverImage(normalized)
+      new ModelDao(ctx.configuration()).update(model)
+      Response.ok(Map("coverImage" -> normalized)).build()
+    }
+  }
+
+  /** 307 redirect to the cover's presigned S3 URL. */
+  @GET
+  @PermitAll
+  @Path("/{mid}/cover")
+  def getModelCover(
+      @PathParam("mid") mid: Integer,
+      @Auth sessionUser: Optional[SessionUser]
+  ): Response = {
+    withTransaction(context) { ctx =>
+      val model = requireCoverReadAccess(ctx, mid, sessionUser)
+      val coverImage = Option(model.getCoverImage).getOrElse(
+        throw new NotFoundException("No cover image")
+      )
+
+      val document = CoverImageUtils
+        .openCover(ResourceType.Model, getOwner(ctx, mid).getEmail, 
model.getName, coverImage)
+        .getOrElse(throw new NotFoundException("No cover image"))
+
+      Response
+        .temporaryRedirect(new URI(CoverImageUtils.presignedUrl(document, 
coverImage)))
+        .build()
+    }
+  }
+
+  /**
+    * Presigned cover URL as JSON. Needed for private models because `<img 
src>`
+    * cannot attach the Authorization header that GET /{mid}/cover requires.
+    */
+  @GET
+  @PermitAll
+  @Path("/{mid}/cover-url")
+  @Produces(Array(MediaType.APPLICATION_JSON))
+  def getModelCoverUrl(
+      @PathParam("mid") mid: Integer,
+      @Auth sessionUser: Optional[SessionUser]
+  ): Response = {
+    withTransaction(context) { ctx =>
+      val model = requireCoverReadAccess(ctx, mid, sessionUser)
+
+      Option(model.getCoverImage) match {
+        case None => Response.ok(Map("url" -> null)).build()
+        case Some(coverImage) =>
+          val url = CoverImageUtils
+            .openCover(ResourceType.Model, getOwner(ctx, mid).getEmail, 
model.getName, coverImage)
+            .map(CoverImageUtils.presignedUrl(_, coverImage))
+          Response.ok(Map("url" -> url.orNull)).build()
+      }
+    }
+  }
+
   // 
===========================================================================
   // Private helpers
   // 
===========================================================================
 
+  /** A cover is readable by anyone for a public model, and by read-grantees 
otherwise. */
+  private def requireCoverReadAccess(
+      ctx: DSLContext,
+      mid: Integer,
+      sessionUser: Optional[SessionUser]
+  ): Model = {
+    val model = getModelByID(ctx, mid)
+    val requesterUid = if (sessionUser.isPresent) 
Some(sessionUser.get().getUid) else None
+
+    if (requesterUid.isEmpty && !model.getIsPublic) {
+      throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
+    } else if (requesterUid.exists(uid => !userHasReadAccess(ctx, mid, uid))) {
+      throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_MODEL_MESSAGE)
+    }
+    model
+  }

Review Comment:
   Agreed, and it was worse than you counted — `getDashboardDataset` has the 
same five lines, so it was five copies, not four. The blast-radius argument is 
the right one: a later tightening of `getDashboardModel` would have left the 
cover endpoints handing out presigned URLs under the old rule with nothing 
failing.
   
   Added `requireReadAccess` to both resources as you suggested. 
`getDashboardModel`, `getDashboardDataset` and all three cover endpoints go 
through it; `requireCoverReadAccess` stays as the `Optional[SessionUser]` to 
`Option[Integer]` adapter. One copy per resource now.
   
   300 tests pass, including the dataset permissions spec 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to