You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Comprehensive reference for the JWST Data Analysis application's security model: user roles, data visibility, endpoint authorization, and access control patterns.
Last updated: 2026-04-20 (#1173 enumeration hardening for POST /generate-and-save)
User Roles
Role
How Assigned
Description
Admin
Role claim = "Admin" in JWT
Full access to all data and operations. Can bypass ownership checks.
Authenticated User
Valid JWT with sub / NameIdentifier claim
Can create, own, and manage their own data. Can read public and shared data.
Anonymous
No JWT / unauthenticated
Read-only access to public data. Cannot create, modify, or delete anything.
Data Access Control Fields
Each JwstDataRecord in MongoDB has four fields that determine who can access it:
Field
Type
Default
Purpose
UserId
string?
null
Owner of the record. Set at import/upload time.
IsPublic
bool
false
When true, readable by everyone including anonymous users.
SharedWith
List<string>
[]
Additional user IDs granted read access to private data.
IsArchived
bool
false
Soft-delete flag. Hidden from default listings but still accessible by ID.
Design decision: IsPublic defaults to false (secure by default). User-uploaded data is private until explicitly shared. MAST-imported data explicitly sets IsPublic = true since JWST observations are public domain.
Access Control Matrix
Data Read Access
Who can view/read a record (GET by ID, preview, histogram, pixel data, thumbnail, file download, spectral data, analysis):
Data State
Admin
Owner
Shared User
Other Auth User
Anonymous
Public (IsPublic=true)
Yes
Yes
Yes
Yes
Yes
Private, shared (IsPublic=false, in SharedWith)
Yes
Yes
Yes
No
No
Private, not shared (IsPublic=false, empty SharedWith)
Yes
Yes
No
No
No
Archived + Public
Yes
Yes
Yes
Yes
Yes
Archived + Private
Yes
Yes
Shared only
No
No
Response for denied access: Most endpoints return 404 Not Found (not 403 Forbidden) to prevent ID enumeration.
Data Mutation Access
Who can modify a record (update metadata, share/publish, archive, unarchive):
Action
Admin
Owner
Shared User
Other Auth User
Anonymous
Update metadata (PUT)
Yes
Yes
No (403)
No (403)
No (401)
Share / change visibility
Yes
Yes
No (403)
No (403)
No (401)
Archive
Yes
Yes
No (403)
No (403)
No (401)
Unarchive
Yes
Yes
No (403)
No (403)
No (401)
Data Deletion Access
Who can delete data:
Action
Admin
Owner
Shared User
Other Auth User
Anonymous
Delete single record
Yes
Yes
No (403)
No (403)
No (401)
Delete entire observation
Yes
Yes (all files must be owned)
No (403)
No (403)
No (401)
Delete observation level
Yes
Yes (all files must be owned)
No (403)
No (403)
No (401)
Archive observation level
Yes
Yes (all files must be owned)
No (403)
No (403)
No (401)
Data Creation Access
Action
Admin
Auth User
Anonymous
Upload FITS file
Yes
Yes (becomes owner)
No (401)
Import from MAST
Yes
Yes (becomes owner)
No (401)
Create via POST
Yes
Yes (becomes owner)
No (401)
Computed/Generated Data
Action
Admin
Auth User (accessible inputs)
Auth User (inaccessible inputs)
Anonymous (public inputs)
Anonymous (private inputs)
Generate mosaic
Yes
Yes
403
Public inputs only
No
Generate composite
Yes
Yes
404
Public inputs only
No
Run analysis
Yes
Yes
403
Public inputs only
No
Export (mosaic/composite)
Yes
Yes
—
No (401)
No (401)
Authorization Helpers
All controllers inherit from ApiControllerBase, which provides identity extraction. Access control helpers are in individual controllers:
Base Class (ApiControllerBase)
Method
Returns
Logic
GetCurrentUserId()
string?
Reads NameIdentifier or sub claim from JWT. Returns null for unauthenticated.
GetRequiredUserId()
string
Same, but throws UnauthorizedAccessException if null. Use in [Authorize] endpoints.
IsCurrentUserAdmin()
bool
User.IsInRole("Admin")
Controller-Level Helpers
Method
Location
Logic
IsDataAccessible(record)
JwstDataController, AnalysisController
Unauthenticated → IsPublic only. Authenticated → IsPublic OR owner OR SharedWith OR admin.
CanModifyData(record)
JwstDataController
owner OR admin. Shared users cannot modify.
CanAccessData(record)
JwstDataController
Authenticated-only variant: IsPublic OR owner OR SharedWith OR admin.
FilterAccessibleData(list)
JwstDataController, DataManagementController
Filters a list to only records the current user can access.
Same logic as controller version, but accepts explicit user context (for background jobs).
Endpoint Authorization Reference
Legend
Open: No authentication required ([AllowAnonymous])
Auth: Requires valid JWT ([Authorize])
Admin: Requires admin role ([Authorize(Policy="AdminOnly")])
+ Access Check: Endpoint performs per-record authorization beyond the attribute
AuthController (/api/auth)
Endpoint
Auth
Internal Check
Notes
POST /login
Open
None
POST /register
Open
None
POST /refresh
Open
None
POST /logout
Auth
UserId null-check
GET /me
Auth
UserId null-check
POST /change-password
Auth
UserId null-check
Own password only
JwstDataController (/api/jwstdata)
Endpoint
Auth
Internal Check
Notes
GET /
Open
+ FilterAccessibleData
Anon → public; Auth → own+public+shared; Admin → all
GET /{id}
Open
+ IsDataAccessible
404 if inaccessible
GET /{id}/preview
Open
+ IsDataAccessible
GET /{id}/histogram
Open
+ IsDataAccessible
GET /{id}/pixeldata
Open
+ IsDataAccessible
GET /{id}/cubeinfo
Open
+ IsDataAccessible
GET /{id}/file
Open
+ IsDataAccessible
GET /{id}/processing-results
Open
+ IsDataAccessible
GET /{id}/thumbnail
Open
+ IsDataAccessible
Returns 404 (not 403) to prevent enumeration
GET /type/{dataType}
Open
+ FilterAccessibleData
GET /status/{status}
Open
+ FilterAccessibleData
GET /tags/{tags}
Open
+ FilterAccessibleData
GET /statistics
Open
None
Aggregate counts only
GET /public
Open
None
DB query for public records
GET /validated
Open
+ FilterAccessibleData
GET /format/{fileFormat}
Open
+ FilterAccessibleData
GET /tags
Open
None
Tag list only
GET /lineage/{obsBaseId}
Open
+ FilterAccessibleData
GET /lineage
Open
+ FilterAccessibleData
GET /archived
Auth
+ FilterAccessibleData
GET /user/{userId}
Auth
Own userId or admin
Non-admin gets 403 for other users
POST /
Auth
Sets UserId to current
POST /upload
Auth
Sets UserId to current
POST /search
Auth
Non-admin filtered to own+public
POST /{id}/share
Auth
+ CanModifyData
Owner or admin
POST /{id}/archive
Auth
+ CanModifyData
Owner or admin
POST /{id}/unarchive
Auth
+ CanModifyData
Owner or admin
POST /check-availability
Open
+ FilterAccessibleData
PUT /{id}
Auth
+ CanModifyData
Owner or admin
DELETE /{id}
Auth
+ CanModifyData
Owner or admin
DELETE /observation/{obsBaseId}
Auth
All records must be owned (or admin)
DELETE /observation/{obsBaseId}/level/{level}
Auth
All records must be owned (or admin)
POST /observation/{obsBaseId}/level/{level}/archive
Auth
All records must be owned (or admin)
POST /generate-thumbnails
Admin
Admin policy
POST /bulk/tags
Admin
Admin policy
POST /bulk/status
Admin
Admin policy
POST /migrate/processing-levels
Admin
Admin policy
POST /migrate/data-types
Admin
Admin policy
AnalysisController (/api/analysis)
Endpoint
Auth
Internal Check
Notes
POST /region-statistics
Open
+ IsDataAccessible
Anon: 404 if inaccessible (anti-enumeration); Auth: 403
POST /detect-sources
Open
+ IsDataAccessible
Anon: 404 if inaccessible (anti-enumeration); Auth: 403
GET /table-info
Open
+ IsDataAccessible
Anon: 404 if inaccessible (anti-enumeration); Auth: 403
GET /table-data
Open
+ IsDataAccessible
Anon: 404 if inaccessible (anti-enumeration); Auth: 403
GET /spectral-data
Open
+ IsDataAccessible
Anon: 404 if inaccessible (anti-enumeration); Auth: 403
MosaicController (/api/mosaic)
Endpoint
Auth
Internal Check
Notes
POST /generate
Open
+ Service-level CanAccessData per input
Anon: public data only, returns 404 (not 403) for inaccessible
POST /generate-and-save
Auth
+ Service-level CanAccessData per input
Auth: 403 if accessible; 404 if inaccessible (defensive anti-enumeration if later [AllowAnonymous])
POST /footprint
Open
+ Service-level CanAccessData per input
Anon: public data only, returns 404 (not 403) for inaccessible
POST /export
Auth
UserId null-check
Operates on generated output
POST /save
Auth
UserId null-check
GET /limits
Open
None
Configuration values only
CompositeController (/api/composite)
Endpoint
Auth
Internal Check
Notes
POST /generate-nchannel
Open
+ Service-level access check per input
404 if inaccessible to anon
POST /export-nchannel
Auth
UserId null-check
POST /analyze-channels
Open
+ Service-level access check per input
404 if inaccessible to anon
MastController (/api/mast)
Endpoint
Auth
Internal Check
Notes
POST /search/target
Open
None
MAST catalog query
POST /search/coordinates
Open
None
POST /search/observation
Open
None
POST /search/program
Open
None
POST /whats-new
Open
None
POST /products
Open
None
POST /download
Auth
None
Downloads to server storage
POST /import
Auth
Sets UserId to current
GET /import-progress/{jobId}
Auth
Owner or admin (404 for others)
POST /import/cancel/{jobId}
Auth
Passes userId to cancel
POST /import/resume/{jobId}
Auth
Owner or admin (404 for others)
POST /import/from-existing/{obsId}
Auth
Sets UserId
GET /import/check-files/{obsId}
Auth
None (filesystem check)
GET /import/resumable
Auth
User-scoped via job tracker (admin sees all)
DELETE /import/resumable/{jobId}
Auth
Owner or admin (404 for others)
POST /refresh-metadata/{obsId}
Auth
Owner-scoped (admin refreshes all)
POST /refresh-metadata-all
Admin
Admin policy
DataManagementController (/api/datamanagement)
Endpoint
Auth
Internal Check
Notes
POST /search
Open
+ FilterAccessibleData (includes SharedWith)
GET /statistics
Open
None (aggregates)
GET /public
Open
None (public query)
GET /validated
Open
+ FilterAccessibleData
GET /format/{fileFormat}
Open
+ FilterAccessibleData
GET /tags
Open
None (tag list)
POST /export
Auth
+ FilterAccessibleData
GET /export/{exportId}
Auth
Owner or admin (404 for others)
Legacy exports without metadata remain accessible
POST /import/scan
Auth
None
POST /claim-orphaned
Auth
Sets UserId
POST /bulk/tags
Admin
Admin policy
POST /bulk/status
Admin
Admin policy
POST /migrate-storage-keys
Admin
Admin policy
DiscoveryController (/api/discovery)
Endpoint
Auth
Internal Check
Notes
GET /featured
Open
None
Curated content
POST /suggest-recipes
Open
None
AI suggestions
JobsController (/api/jobs)
Endpoint
Auth
Internal Check
Notes
GET /
Auth
User-scoped query
Only own jobs (even for admin)
GET /{jobId}
Auth
Owner check (404)
POST /{jobId}/cancel
Auth
Owner check
GET /{jobId}/result
Auth
Owner check (404)
SearchController (/api/search)
Endpoint
Auth
Internal Check
Notes
GET /semantic
Open
+ Per-record access filtering
Filters results by IsPublic, owner, SharedWith, admin
POST /reindex
Admin
Admin policy
Triggers full re-index of all documents
GET /index-status
Open
None
Index health info only
Python Processing Engine (internal)
The processing engine runs as an internal service behind the .NET API gateway. It has no authentication layer — all requests are trusted as pre-authorized by the .NET layer.
Previously tracked gaps #565-#570 were resolved in PR #573.
Design Decisions & Rationale
IsPublic defaults to true: JWST data is public domain. Import from MAST creates public records unless the user explicitly makes them private.
404 over 403 for read access: Most read endpoints return 404 Not Found for inaccessible data rather than 403 Forbidden. This prevents ID enumeration attacks — an attacker cannot distinguish "exists but private" from "does not exist".
Processing engine has no auth: The Python processing engine is an internal service not exposed to the internet. The .NET API acts as the auth gateway. This is a trust boundary — if the processing engine were ever exposed directly, it would need its own auth layer.
Service-level auth for background jobs: Mosaic and composite generation can run as background jobs. Since background jobs have no HTTP context, user identity is serialized into the job payload (UserId, IsAuthenticated, IsAdmin) and checked at the service layer.
Owner-only mutations, admin bypass: Only the record owner can modify or delete data. Admin can bypass all ownership checks. Shared users get read access only — they cannot modify shared data.
Observation-level deletes require full ownership: Deleting an entire observation or processing level requires that ALL records in the set belong to the requesting user. This prevents partial-ownership situations where one user could delete another's records in a shared observation.