feat(file-service): add model management API and unify shared resource access - #6869
feat(file-service): add model management API and unify shared resource access#6869tanishqgandhi1908 wants to merge 50 commits into
Conversation
… tests Review fixes on apache#5911: use dataset owner email in retrieveLatestDatasetVersion, replace brittle Option.get/.head with headOption, strip datasets prefix on selection-modal reopen, clarify FileResolver docs; add FileResolver/DatasetFileNode and frontend path-helper tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The datasets logical-path prefix strips four leading segments (datasets/owner/dataset/version); update the cover-image test's input path to include the prefix so the extracted relative path is the file name, not an empty string. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that models are coming as a separate resource/table, the leading resource-type segment is what selects the backing table, so an unprefixed path can no longer be routed unambiguously. Make the "datasets" prefix required instead of a tolerated fallback, and migrate existing data. - FileResolver: a dataset path must start with the "datasets" segment; unprefixed paths are no longer treated as dataset paths. - pytexera DatasetFileDocument: same rule, mirroring the backend. - FileListerSourceOpExec: parse the now-prefixed datasetVersionPath (skip the "datasets" segment); extracted into a testable helper. - Migration (sql/updates/29.sql): prepend "datasets/" to legacy paths stored in workflow.content and workflow_version.content, covering both the fileName (scan sources) and datasetVersionPath (file lister) operator properties. Only values whose first two segments match an existing (user.email, dataset.name) are rewritten, so local paths and URLs are left untouched; email format is irrelevant (owner may be a username without "@"). Uses create_missing=false and is idempotent. - Example workflows: use datasets-prefixed paths. Tests: FileResolverSpec and WorkflowExecutionsResourceSpec updated; test_dataset_file_document.py updated; new FileListerSourceOpExecSpec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l-path-prefix # Conflicts: # frontend/src/app/common/type/datasetVersionFileTree.spec.ts
…format python test - dataset-selection-modal.component.spec.ts: expect the datasets-prefixed selectedPath in version (non-file) mode, matching the emitted path. - test_dataset_file_document.py: apply ruff format (wrap an over-length line) so `ruff format --check` passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cover-image endpoints resolve a dataset path built as
{owner}/{name}/{coverImage}. With the prefix now required by FileResolver,
these must carry the datasets/ segment; add it in the set-cover,
get-cover redirect, and cover-url handlers so cover images resolve again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…storage # Conflicts: # sql/changelog.xml
…rage # Conflicts: # common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/DocumentFactorySpec.scala
…var rename The rename to FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT pushed a few lines past black's 88-col limit; reformat to satisfy the pyamber black --check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…at/model-management-api
…nt-api # Conflicts: # common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala # common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocument.scala # common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/DocumentFactorySpec.scala # common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocumentSpec.scala # sql/changelog.xml # sql/texera_ddl.sql
aicam
left a comment
There was a problem hiding this comment.
LGTM! please address the comments. Also please keep two notes on this PR and add in the description:
1- We will most likely change the prefix from datasets to dataset
2- We also need to discuss if we should go with one table, I see lots of duplicate code
Please keep these two in mind to discuss further in the discussion.
|
/request-review @mengw15 |
There was a problem hiding this comment.
Pull request overview
Adds model metadata and access-management APIs while generalizing dataset/model resource handling.
Changes:
- Adds model access-control schema and REST resources.
- Shares naming and authorization logic across datasets and models.
- Unifies LakeFS document routing with expanded tests.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
sql/updates/38.sql |
Adds model access migration. |
sql/texera_ddl.sql |
Adds model access table to base schema. |
sql/changelog.xml |
Registers changeSet 38. |
file-service/.../ModelResourceSpec.scala |
Tests model management APIs. |
file-service/.../ModelAccessResourceSpec.scala |
Tests model authorization and sharing. |
file-service/.../DatasetAccessResourceSpec.scala |
Updates dataset access tests. |
file-service/.../ResourceNaming.scala |
Centralizes resource naming rules. |
file-service/.../ResourceAccess.scala |
Centralizes resource authorization. |
file-service/.../ModelResource.scala |
Implements model metadata APIs. |
file-service/.../ModelAccessResource.scala |
Implements model sharing APIs. |
file-service/.../ManagedResource.scala |
Describes shared resource tables. |
file-service/.../DatasetResource.scala |
Adopts shared resource helpers. |
file-service/.../DatasetAccessResource.scala |
Adopts shared access logic. |
file-service/.../FileService.scala |
Registers model resources. |
common/.../LakeFSFileDocumentSpec.scala |
Tests unified LakeFS documents. |
common/.../DocumentFactorySpec.scala |
Tests resource-specific routing. |
common/.../ModelFileDocument.scala |
Removes model-specific wrapper. |
common/.../LakeFSFileDocument.scala |
Adds resource-aware endpoint selection. |
common/.../DatasetFileDocument.scala |
Removes dataset-specific wrapper. |
common/.../DocumentFactory.scala |
Creates unified LakeFS documents. |
Suppressed comments (5)
file-service/src/main/scala/org/apache/texera/service/resource/ResourceAccess.scala:197
- Ownership alone is not included in this query, although
userHasReadAccesstreats owners as authorized. Because any WRITE grantee can revoke the owner's explicit row, an owner can then lose the resource from/listwhile still being able to fetch it directly. Join only the caller's grant and select rows where the caller is either owner or grantee; report WRITE for owners.
.where(resource.accessUidField.eq(uid))
file-service/src/main/scala/org/apache/texera/service/resource/ResourceAccess.scala:147
- This read guard lets any authenticated user enumerate every grantee's name and email for a public model, because public resources satisfy
userHasReadAccess. The PR contract says these access-management endpoints are write-guarded; require write access before returning the sharing list.
requireReadAccess(ctx, resource, id, requesterUid)
file-service/src/main/scala/org/apache/texera/service/resource/ResourceAccess.scala:251
- The path parameter is converted without validation. Unknown values throw
IllegalArgumentExceptionas a 500, while the valid enum valueNONEcreates a row thatlistVisibletreats as accessible even though read checks reject it. Accept only READ or WRITE and return a 400 for everything else.
val granted = PrivilegeEnum.valueOf(privilege)
file-service/src/main/scala/org/apache/texera/service/resource/ResourceAccess.scala:275
- Revoking an unknown email dereferences
nulland returns a 500. Since this method documents revocation of a missing grant as a no-op, an email with no account should also return success rather than crashing.
val granteeUid = new UserDao(ctx.configuration()).fetchOneByEmail(email).getUid
file-service/src/main/scala/org/apache/texera/service/resource/ModelResource.scala:223
- The LakeFS/S3 deletions happen before the database row is deleted or the transaction commits. If the S3 call, database delete, or commit fails after
deleteRepo, the transaction preserves a model row whose repository is already gone; retries then stop on LakeFS 404, leaving the model undeletable. Use a persisted deleting state/outbox, or otherwise make cleanup retryable before finalizing the database deletion.
withLakeFSErrorHandling(s"deleting the repository of model '${model.getName}'") {
LakeFSStorageClient.deleteRepo(model.getRepositoryName)
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| CREATE TABLE IF NOT EXISTS model_user_access | ||
| ( | ||
| mid INT NOT NULL, | ||
| uid INT NOT NULL, | ||
| privilege privilege_enum NOT NULL DEFAULT 'NONE', | ||
| PRIMARY KEY (mid, uid), | ||
| FOREIGN KEY (mid) REFERENCES model(mid) ON DELETE CASCADE, | ||
| FOREIGN KEY (uid) REFERENCES "user"(uid) ON DELETE CASCADE | ||
| ); |
| def presignEndpointOf(resourceType: ResourceType.Value): String = | ||
| resourceType match { | ||
| case ResourceType.Datasets => datasetPresignEndpoint | ||
| case ResourceType.Models => modelPresignEndpoint |
| withLakeFSErrorHandling(s"creating the repository of model '${model.getName}'") { | ||
| LakeFSStorageClient.initRepo(repositoryName) |
| .orNull | ||
| } | ||
|
|
||
| /** The owner's email, or an empty string when the resource does not exist. */ |
|
Can we split this pr? Since the change seems a lot |
|
@tanishqgandhi1908 I also think this PR is a bit too big. Can you split it? Also try to provide a more human-readable description at the beginning of the PR. |
Yes, I am working on splitting it such that both PR's are independent. Will update the description too accordingly |
What changes were proposed in this PR?
Adds the metadata + access-control half of the model management API, and unifies the
code datasets and models share. Model version upload follows in a separate PR.
Schema:
model_user_accesstable intexera_ddl.sql+ idempotent migrationsql/updates/31.sql(changeSet 31).ModelResource(@Path("/model"))/createmodel-{mid}, rolls the DB row back if repo init fails, grants the ownerWRITE. Storesframework(defaultspytorch) andformat./{mid}/update/name/update/description/{mid}/update/publicityis_public(write access)./{mid}/update/downloadableis_downloadable(owner-only)./list/{mid}/public/{mid}ModelAccessResource(@Path("/access/model")) —owner/{mid},list/{mid},grant/{mid}/{email}/{privilege},revoke/{mid}/{email}(all write-access-guarded), backed bymodel_user_access.FileService; storage/util layer reused unchanged.Shared access + naming layer (touches dataset code) - Rather than duplicating the dataset rules for models, the shared rules now have one implementation. jOOQ generates an unrelated class per table with no common supertype, so the shared code is parameterized by a descriptor naming the relevant columns instead of by inheritance:
Unified file document -
LakeFSFileDocumentnow takes aResourceTypeand resolves its own presign endpoint, soModelFileDocumentandDatasetFileDocumentare both deleted — one document class serves every resource type.Stacked on #6860 (#6497); its commits show in this diff until that merges.
Any related issues, documentation, discussions?
Part of #6498, umbrella #6494.
How was this PR tested?
New tests:
ModelResourceSpec(22 — create/get/list/delete/update/toggle, incl. accessfiltering, duplicate-name and forbidden paths, LakeFS-404 → 404) and
ModelAccessResourceSpec(30 — privilege helpers, grant/list/revoke, placeholder-account and unknown-email rejection).
DocumentFactorySpecnow asserts the resource type per URI scheme so scheme routing stayscovered, and
DatasetFileDocumentSpecbecameLakeFSFileDocumentSpecwith per-resource-typepresign-endpoint tests.
Because the refactors touch dataset code, the existing dataset suites are the regression guard
and were run in full:
sbt "FileService/test"— 283/283 (incl. upstreamDatasetResourceSpec,DatasetAccessResourceSpec)sbt "WorkflowCore/test"— 761/761scalafmtAll+scalafixAll --checkcleanAlso verified manually in a running Texera: dataset create / rename (incl. renaming to the same
name) / duplicate rejection, sharing (grant READ→WRITE, access list, revoke), public visibility,
owner-only delete and downloadable toggle, and file upload + version creation.
Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 4.8)