diff --git a/api/api_categories.go b/api/api_categories.go index dc291d0..06d2ec3 100644 --- a/api/api_categories.go +++ b/api/api_categories.go @@ -14,6 +14,12 @@ type CategoryCount struct { } func handleCategories(w http.ResponseWriter, r *http.Request) { + // Get accessor (who is asking) + ctx := getAccessContextOrFail(w, r) + if ctx == nil { + return + } + dossierHex := r.URL.Query().Get("dossier") if dossierHex == "" { http.Error(w, "missing dossier", http.StatusBadRequest) @@ -24,15 +30,16 @@ func handleCategories(w http.ResponseWriter, r *http.Request) { obsType := r.URL.Query().Get("type") category := r.URL.Query().Get("category") + // Pass accessor + dossier to lib - RBAC handled there var counts map[string]CategoryCount if obsType == "" { - counts = getTopLevelCounts(dossierID) + counts = getTopLevelCounts(ctx.AccessorID, dossierID) } else if obsType == "genome" { if category != "" { - counts = getGenomeSubcategoryCounts(dossierID, category) + counts = getGenomeSubcategoryCounts(ctx.AccessorID, dossierID, category) } else { - counts = getGenomeCounts(dossierID) + counts = getGenomeCounts(ctx.AccessorID, dossierID) } } else { counts = make(map[string]CategoryCount) @@ -42,28 +49,38 @@ func handleCategories(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(counts) } -func getTopLevelCounts(dossierID string) map[string]CategoryCount { +func getTopLevelCounts(accessorID, dossierID string) map[string]CategoryCount { counts := make(map[string]CategoryCount) - // Query entry counts by category - var catCounts []struct { - Category int `db:"category"` - Count int `db:"cnt"` - } - lib.Query("SELECT category, COUNT(*) as cnt FROM entries WHERE dossier_id = ? AND category > 0 GROUP BY category", []any{dossierID}, &catCounts) - categoryNames := map[int]string{ 1: "imaging", 2: "documents", 3: "labs", 4: "genome", } - for _, c := range catCounts { - if name, ok := categoryNames[c.Category]; ok && c.Count > 0 { - counts[name] = CategoryCount{Shown: c.Count, Hidden: 0} + // Check each category - only include if accessor has access + for catInt, catName := range categoryNames { + // Try to get a count by querying entries with RBAC + entries, err := lib.EntryList(accessorID, "", catInt, &lib.EntryFilter{ + DossierID: dossierID, + Limit: 1, // Just check if we can see any + }) + if err != nil || len(entries) == 0 { + continue // No access or no entries + } + + // Get actual count (using system context for counting only) + var catCounts []struct { + Count int `db:"cnt"` + } + lib.Query("SELECT COUNT(*) as cnt FROM entries WHERE dossier_id = ? AND category = ?", + []any{dossierID, catInt}, &catCounts) + + if len(catCounts) > 0 && catCounts[0].Count > 0 { + counts[catName] = CategoryCount{Shown: catCounts[0].Count, Hidden: 0} } } // For genome, replace with detailed subcategory counts - genomeCats := getGenomeCounts(dossierID) + genomeCats := getGenomeCounts(accessorID, dossierID) if len(genomeCats) > 0 { totalShown, totalHidden := 0, 0 for _, c := range genomeCats { @@ -100,11 +117,11 @@ func shouldIncludeVariant(data variantData, includeHidden bool) bool { } // getGenomeCounts reads cached counts from the extraction entry (fast path) -func getGenomeCounts(dossierID string) map[string]CategoryCount { +func getGenomeCounts(accessorID, dossierID string) map[string]CategoryCount { counts := make(map[string]CategoryCount) - // Use system context for internal counting operations - ctx := &lib.AccessContext{IsSystem: true} + // Create access context for RBAC + ctx := &lib.AccessContext{AccessorID: accessorID} // Find extraction entry and read its data extraction, err := lib.GenomeGetExtraction(ctx, dossierID) @@ -130,15 +147,15 @@ func getGenomeCounts(dossierID string) map[string]CategoryCount { } // Fallback: compute counts (for old data without cached counts) - return getGenomeCountsSlow(dossierID) + return getGenomeCountsSlow(accessorID, dossierID) } // getGenomeCountsSlow computes counts by scanning all variants (fallback for old data) -func getGenomeCountsSlow(dossierID string) map[string]CategoryCount { +func getGenomeCountsSlow(accessorID, dossierID string) map[string]CategoryCount { counts := make(map[string]CategoryCount) - // Use system context for internal counting operations - ctx := &lib.AccessContext{IsSystem: true} + // Create access context for RBAC + ctx := &lib.AccessContext{AccessorID: accessorID} // Find extraction entry extraction, err := lib.GenomeGetExtraction(ctx, dossierID) @@ -179,13 +196,13 @@ func getGenomeCountsSlow(dossierID string) map[string]CategoryCount { return counts } -func getGenomeSubcategoryCounts(dossierID string, category string) map[string]CategoryCount { +func getGenomeSubcategoryCounts(accessorID, dossierID string, category string) map[string]CategoryCount { counts := make(map[string]CategoryCount) shownCounts := make(map[string]int) hiddenCounts := make(map[string]int) - // Use system context for internal counting operations - ctx := &lib.AccessContext{IsSystem: true} + // Create access context for RBAC + ctx := &lib.AccessContext{AccessorID: accessorID} // Find extraction entry extraction, err := lib.GenomeGetExtraction(ctx, dossierID) diff --git a/api/api_v1.go b/api/api_v1.go index a8af215..e1a4d0d 100644 --- a/api/api_v1.go +++ b/api/api_v1.go @@ -231,7 +231,7 @@ func v1Entries(w http.ResponseWriter, r *http.Request, dossierID string) { filter.Limit, _ = strconv.Atoi(limit) } - entries, err := lib.EntryList(nil, parentID, category, filter) // nil ctx - v1 API has own auth + entries, err := lib.EntryList("", parentID, category, filter) // nil ctx - v1 API has own auth if err != nil { v1Error(w, err.Error(), http.StatusInternalServerError) return @@ -305,7 +305,7 @@ func v1Entry(w http.ResponseWriter, r *http.Request, dossierID, entryID string) } // Get children - children, _ := lib.EntryList(nil, entryID, 0, nil) // nil ctx - v1 API has own auth + children, _ := lib.EntryList("", entryID, 0, nil) // nil ctx - v1 API has own auth if len(children) > 0 { var childList []map[string]any for _, c := range children { diff --git a/api/auth.go b/api/auth.go index 9b37151..e9fbbab 100644 --- a/api/auth.go +++ b/api/auth.go @@ -76,7 +76,11 @@ func getAccessContextOrFail(w http.ResponseWriter, r *http.Request) *lib.AccessC // requireDossierAccess checks if the accessor can read the specified dossier. // Returns true if allowed, false and writes 403 if denied. func requireDossierAccess(w http.ResponseWriter, ctx *lib.AccessContext, dossierID string) bool { - if err := lib.CheckAccess(ctx, dossierID, "", 'r'); err != nil { + accessorID := "" + if ctx != nil && !ctx.IsSystem { + accessorID = ctx.AccessorID + } + if err := lib.CheckAccess(accessorID, dossierID, "", 'r'); err != nil { http.Error(w, "Forbidden: access denied to this dossier", http.StatusForbidden) return false } @@ -86,7 +90,11 @@ func requireDossierAccess(w http.ResponseWriter, ctx *lib.AccessContext, dossier // requireEntryAccess checks if the accessor can perform op on the entry. // Returns true if allowed, false and writes 403 if denied. func requireEntryAccess(w http.ResponseWriter, ctx *lib.AccessContext, dossierID, entryID string, op rune) bool { - if err := lib.CheckAccess(ctx, dossierID, entryID, op); err != nil { + accessorID := "" + if ctx != nil && !ctx.IsSystem { + accessorID = ctx.AccessorID + } + if err := lib.CheckAccess(accessorID, dossierID, entryID, op); err != nil { http.Error(w, "Forbidden: access denied", http.StatusForbidden) return false } @@ -96,7 +104,11 @@ func requireEntryAccess(w http.ResponseWriter, ctx *lib.AccessContext, dossierID // requireManageAccess checks if the accessor can manage permissions for a dossier. // Returns true if allowed, false and writes 403 if denied. func requireManageAccess(w http.ResponseWriter, ctx *lib.AccessContext, dossierID string) bool { - if err := lib.CheckAccess(ctx, dossierID, "", 'm'); err != nil { + accessorID := "" + if ctx != nil && !ctx.IsSystem { + accessorID = ctx.AccessorID + } + if err := lib.CheckAccess(accessorID, dossierID, "", 'm'); err != nil { http.Error(w, "Forbidden: manage permission required", http.StatusForbidden) return false } diff --git a/import-dicom/main.go b/import-dicom/main.go index 7561e13..3fabdad 100644 --- a/import-dicom/main.go +++ b/import-dicom/main.go @@ -451,7 +451,7 @@ func getOrCreateStudy(data []byte, dossierID string) (string, error) { } // Query for existing study using V2 API - studies, err := lib.EntryList(nil, "", lib.CategoryImaging, &lib.EntryFilter{ // nil ctx - import tool + studies, err := lib.EntryList("", "", lib.CategoryImaging, &lib.EntryFilter{ // nil ctx - import tool DossierID: dossierID, Type: "study", }) @@ -525,7 +525,7 @@ func getOrCreateSeries(data []byte, dossierID, studyID string) (string, error) { } // Query for existing series using V2 API - children, err := lib.EntryList(nil, studyID, lib.CategoryImaging, &lib.EntryFilter{ // nil ctx - import tool + children, err := lib.EntryList("", studyID, lib.CategoryImaging, &lib.EntryFilter{ // nil ctx - import tool DossierID: dossierID, Type: "series", }) @@ -1192,7 +1192,7 @@ func main() { fmt.Printf("Dossier: %s (DOB: %s)\n", dossier.Name, dob) // Check for existing imaging data - existing, _ := lib.EntryList(nil, "", lib.CategoryImaging, &lib.EntryFilter{DossierID: dossierID, Limit: 1}) // nil ctx - import tool + existing, _ := lib.EntryList("", "", lib.CategoryImaging, &lib.EntryFilter{DossierID: dossierID, Limit: 1}) // nil ctx - import tool if len(existing) > 0 { fmt.Printf("Clean existing imaging data? (yes/no): ") reader := bufio.NewReader(os.Stdin) diff --git a/lib/access.go b/lib/access.go index afe6cb9..6e52a8d 100644 --- a/lib/access.go +++ b/lib/access.go @@ -127,53 +127,41 @@ func InvalidateCacheAll() { // checkAccess is the internal permission check called by v2.go data functions. // Returns nil if allowed, ErrAccessDenied if not. // +// Parameters: +// accessorID - who is asking (empty = system/internal) +// dossierID - whose data +// entryID - specific entry (empty = root level) +// op - operation: 'r', 'w', 'd', 'm' +// // Algorithm: -// 1. ctx == nil → allow (backward compatibility, internal operations) -// 2. ctx.IsSystem → allow -// 3. Accessor == dossier owner → allow (full access to own data) -// 4. Check grants for accessor on this dossier: -// a. Entry-specific grant (entry_id matches) -// b. Walk up parent_id chain checking each level -// c. Root grant (entry_id = "") -// 5. No matching grant → deny -func checkAccess(ctx *AccessContext, dossierID, entryID string, op rune) error { - // 1. nil context allows (for internal operations that pass nil) - if ctx == nil { +// 1. Empty accessor → allow (system/internal operations) +// 2. Accessor == owner → allow (full access to own data) +// 3. Check grants (entry-specific → parent chain → root) +// 4. No grant → deny +func checkAccess(accessorID, dossierID, entryID string, op rune) error { + // 1. Empty accessor = system/internal operation + if accessorID == "" { return nil } - // 2. System context bypasses all checks - if ctx.IsSystem { + // 2. Owner has full access to own data + if accessorID == dossierID { return nil } - // Must have accessor for non-system context - if ctx.AccessorID == "" { - return ErrNoAccessor - } - - // 3. Owner has full access to own data - if ctx.AccessorID == dossierID { - return nil - } - - // 4. Check grants - ops := getEffectiveOps(ctx.AccessorID, dossierID, entryID) + // 3. Check grants + ops := getEffectiveOps(accessorID, dossierID, entryID) if hasOp(ops, op) { return nil } - // 5. Deny + // 4. No grant found - deny return ErrAccessDenied } // CheckAccess is the exported version for use by API/Portal code. -// Same algorithm as checkAccess but requires non-nil context. -func CheckAccess(ctx *AccessContext, dossierID, entryID string, op rune) error { - if ctx == nil { - return ErrNoAccessor - } - return checkAccess(ctx, dossierID, entryID, op) +func CheckAccess(accessorID, dossierID, entryID string, op rune) error { + return checkAccess(accessorID, dossierID, entryID, op) } // getEffectiveOps returns the ops string for accessor on dossier/entry @@ -312,8 +300,8 @@ func accessGrantListRaw(f *PermissionFilter) ([]*Access, error) { // EnsureCategoryEntry creates a category entry if it doesn't exist // Returns the entry_id of the category entry func EnsureCategoryEntry(dossierID string, category int) (string, error) { - // Check if category entry already exists - entries, err := EntryList(SystemContext, "", category, &EntryFilter{ + // Check if category entry already exists (use empty string for system context) + entries, err := EntryList("", "", category, &EntryFilter{ DossierID: dossierID, Type: "category", Limit: 1, @@ -340,13 +328,13 @@ func EnsureCategoryEntry(dossierID string, category int) (string, error) { } // CanAccessDossier returns true if accessor can read dossier (for quick checks) -func CanAccessDossier(ctx *AccessContext, dossierID string) bool { - return CheckAccess(ctx, dossierID, "", 'r') == nil +func CanAccessDossier(accessorID, dossierID string) bool { + return CheckAccess(accessorID, dossierID, "", 'r') == nil } // CanManageDossier returns true if accessor can manage permissions for dossier -func CanManageDossier(ctx *AccessContext, dossierID string) bool { - return CheckAccess(ctx, dossierID, "", 'm') == nil +func CanManageDossier(accessorID, dossierID string) bool { + return CheckAccess(accessorID, dossierID, "", 'm') == nil } // GrantAccess creates an access grant diff --git a/lib/roles.go b/lib/roles.go index 850b960..bfe21c4 100644 --- a/lib/roles.go +++ b/lib/roles.go @@ -138,8 +138,8 @@ func ApplyRoleTemplate(dossierID, granteeID, roleName string) error { // findOrCreateCategoryRoot finds or creates a root entry for category-level grants // This is a virtual entry that serves as parent for all entries of that category func findOrCreateCategoryRoot(dossierID string, category int) (string, error) { - // Look for existing category root entry (type = "category_root") - entries, err := EntryList(nil, "", category, &EntryFilter{ + // Look for existing category root entry (type = "category_root", use empty string for system context) + entries, err := EntryList("", "", category, &EntryFilter{ DossierID: dossierID, Type: "category_root", Limit: 1, diff --git a/lib/v2.go b/lib/v2.go index e80f6b4..de8768a 100644 --- a/lib/v2.go +++ b/lib/v2.go @@ -28,6 +28,15 @@ import ( // // ============================================================================ +// accessorIDFromContext extracts accessorID from AccessContext for RBAC checks +// Returns empty string for system context (which bypasses checks) +func accessorIDFromContext(ctx *AccessContext) string { + if ctx == nil || ctx.IsSystem { + return "" + } + return ctx.AccessorID +} + // --- ENTRY --- type EntryFilter struct { @@ -51,7 +60,7 @@ func EntryWrite(ctx *AccessContext, entries ...*Entry) error { return fmt.Errorf("entry missing dossier_id") } // Check write on parent (or root if no parent) - if err := checkAccess(ctx, e.DossierID, e.ParentID, 'w'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), e.DossierID, e.ParentID, 'w'); err != nil { return err } } @@ -75,7 +84,7 @@ func EntryRemove(ctx *AccessContext, ids ...string) error { if err != nil { continue // Entry doesn't exist, skip } - if err := checkAccess(ctx, e.DossierID, id, 'd'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), e.DossierID, id, 'd'); err != nil { return err } } @@ -85,7 +94,7 @@ func EntryRemove(ctx *AccessContext, ids ...string) error { // EntryRemoveByDossier removes all entries for a dossier. Requires delete permission on dossier root. func EntryRemoveByDossier(ctx *AccessContext, dossierID string) error { // RBAC: Check delete permission on dossier root - if err := checkAccess(ctx, dossierID, "", 'd'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), dossierID, "", 'd'); err != nil { return err } @@ -109,7 +118,7 @@ func EntryGet(ctx *AccessContext, id string) (*Entry, error) { } // RBAC: Check read permission - if err := checkAccess(ctx, e.DossierID, id, 'r'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), e.DossierID, id, 'r'); err != nil { return nil, err } @@ -123,7 +132,9 @@ func entryGetRaw(id string) (*Entry, error) { } // EntryList retrieves entries. Requires read permission on parent/dossier. -func EntryList(ctx *AccessContext, parent string, category int, f *EntryFilter) ([]*Entry, error) { +// accessorID: who is asking (empty = system) +// dossierID comes from f.DossierID +func EntryList(accessorID string, parent string, category int, f *EntryFilter) ([]*Entry, error) { // RBAC: Determine dossier and check read permission dossierID := "" if f != nil { @@ -136,7 +147,7 @@ func EntryList(ctx *AccessContext, parent string, category int, f *EntryFilter) } } if dossierID != "" { - if err := checkAccess(ctx, dossierID, parent, 'r'); err != nil { + if err := checkAccess(accessorID, dossierID, parent, 'r'); err != nil { return nil, err } } @@ -208,7 +219,7 @@ func DossierWrite(ctx *AccessContext, dossiers ...*Dossier) error { for _, d := range dossiers { if d.DossierID != "" { // Update - need manage permission (unless creating own or system) - if err := checkAccess(ctx, d.DossierID, "", 'm'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), d.DossierID, "", 'm'); err != nil { return err } } @@ -234,7 +245,7 @@ func DossierWrite(ctx *AccessContext, dossiers ...*Dossier) error { func DossierRemove(ctx *AccessContext, ids ...string) error { // RBAC: Check manage permission for each dossier for _, id := range ids { - if err := checkAccess(ctx, id, "", 'm'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), id, "", 'm'); err != nil { return err } } @@ -244,7 +255,7 @@ func DossierRemove(ctx *AccessContext, ids ...string) error { // DossierGet retrieves a dossier. Requires read permission. func DossierGet(ctx *AccessContext, id string) (*Dossier, error) { // RBAC: Check read permission - if err := checkAccess(ctx, id, "", 'r'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), id, "", 'r'); err != nil { return nil, err } @@ -541,7 +552,7 @@ func ImageGet(ctx *AccessContext, id string, opts *ImageOpts) ([]byte, error) { } // RBAC: Check read permission - if err := checkAccess(ctx, e.DossierID, id, 'r'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), e.DossierID, id, 'r'); err != nil { return nil, err } @@ -626,7 +637,7 @@ func ImageGet(ctx *AccessContext, id string, opts *ImageOpts) ([]byte, error) { // ObjectWrite encrypts and writes data to the object store. Requires write permission. func ObjectWrite(ctx *AccessContext, dossierID, entryID string, data []byte) error { // RBAC: Check write permission - if err := checkAccess(ctx, dossierID, entryID, 'w'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), dossierID, entryID, 'w'); err != nil { return err } @@ -641,7 +652,7 @@ func ObjectWrite(ctx *AccessContext, dossierID, entryID string, data []byte) err // ObjectRead reads and decrypts data from the object store. Requires read permission. func ObjectRead(ctx *AccessContext, dossierID, entryID string) ([]byte, error) { // RBAC: Check read permission - if err := checkAccess(ctx, dossierID, entryID, 'r'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), dossierID, entryID, 'r'); err != nil { return nil, err } @@ -664,7 +675,7 @@ func objectReadRaw(dossierID, entryID string) ([]byte, error) { // ObjectRemove deletes an object from the store. Requires delete permission. func ObjectRemove(ctx *AccessContext, dossierID, entryID string) error { // RBAC: Check delete permission - if err := checkAccess(ctx, dossierID, entryID, 'd'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), dossierID, entryID, 'd'); err != nil { return err } return os.Remove(ObjectPath(dossierID, entryID)) @@ -673,7 +684,7 @@ func ObjectRemove(ctx *AccessContext, dossierID, entryID string) error { // ObjectRemoveByDossier removes all objects for a dossier. Requires delete permission. func ObjectRemoveByDossier(ctx *AccessContext, dossierID string) error { // RBAC: Check delete permission on dossier root - if err := checkAccess(ctx, dossierID, "", 'd'); err != nil { + if err := checkAccess(accessorIDFromContext(ctx), dossierID, "", 'd'); err != nil { return err } return os.RemoveAll(filepath.Join(ObjectDir, dossierID)) @@ -930,7 +941,7 @@ func AccessRevokeEntry(dossierID, granteeID, entryID string) error { // GenomeGetExtraction returns the extraction entry for genome data func GenomeGetExtraction(ctx *AccessContext, dossierID string) (*Entry, error) { - entries, err := EntryList(ctx, "", CategoryGenome, &EntryFilter{ + entries, err := EntryList(accessorIDFromContext(ctx), "", CategoryGenome, &EntryFilter{ DossierID: dossierID, Type: "extraction", Limit: 1, @@ -949,7 +960,7 @@ type GenomeTier struct { // GenomeGetTiers returns all tier entries for a genome extraction func GenomeGetTiers(ctx *AccessContext, dossierID, extractionID string) ([]GenomeTier, error) { - entries, err := EntryList(ctx, extractionID, CategoryGenome, &EntryFilter{ + entries, err := EntryList(accessorIDFromContext(ctx), extractionID, CategoryGenome, &EntryFilter{ DossierID: dossierID, Type: "tier", }) @@ -969,7 +980,7 @@ func GenomeGetTiers(ctx *AccessContext, dossierID, extractionID string) ([]Genom // GenomeGetTierByCategory returns a specific tier by category name func GenomeGetTierByCategory(ctx *AccessContext, dossierID, extractionID, category string) (*GenomeTier, error) { - entries, err := EntryList(ctx, extractionID, CategoryGenome, &EntryFilter{ + entries, err := EntryList(accessorIDFromContext(ctx), extractionID, CategoryGenome, &EntryFilter{ DossierID: dossierID, Type: "tier", Value: category, @@ -1008,7 +1019,7 @@ func GenomeGetVariants(ctx *AccessContext, dossierID string, tierIDs []string) ( var variants []GenomeVariant for _, tierID := range tierIDs { - entries, err := EntryList(ctx, tierID, CategoryGenome, &EntryFilter{DossierID: dossierID}) + entries, err := EntryList(accessorIDFromContext(ctx), tierID, CategoryGenome, &EntryFilter{DossierID: dossierID}) if err != nil { continue } @@ -1046,7 +1057,7 @@ func GenomeGetVariants(ctx *AccessContext, dossierID string, tierIDs []string) ( // GenomeGetVariantsByTier returns variants for a specific tier func GenomeGetVariantsByTier(ctx *AccessContext, dossierID, tierID string) ([]GenomeVariant, error) { - entries, err := EntryList(ctx, tierID, CategoryGenome, &EntryFilter{DossierID: dossierID}) + entries, err := EntryList(accessorIDFromContext(ctx), tierID, CategoryGenome, &EntryFilter{DossierID: dossierID}) if err != nil { return nil, err } diff --git a/portal/dossier_sections.go b/portal/dossier_sections.go index 2bf3183..9bf82f4 100644 --- a/portal/dossier_sections.go +++ b/portal/dossier_sections.go @@ -126,11 +126,11 @@ func BuildDossierSections(targetID, targetHex string, target *lib.Dossier, p *li // Count trackable categories stats := make(map[string]int) - vitals, _ := lib.EntryList(nil, "", lib.CategoryVital, &lib.EntryFilter{DossierID: targetID, Limit: 1}) - meds, _ := lib.EntryList(nil, "", lib.CategoryMedication, &lib.EntryFilter{DossierID: targetID, Limit: 1}) - supps, _ := lib.EntryList(nil, "", lib.CategorySupplement, &lib.EntryFilter{DossierID: targetID, Limit: 1}) - exercise, _ := lib.EntryList(nil, "", lib.CategoryExercise, &lib.EntryFilter{DossierID: targetID, Limit: 1}) - symptoms, _ := lib.EntryList(nil, "", lib.CategorySymptom, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + vitals, _ := lib.EntryList("", "", lib.CategoryVital, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + meds, _ := lib.EntryList("", "", lib.CategoryMedication, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + supps, _ := lib.EntryList("", "", lib.CategorySupplement, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + exercise, _ := lib.EntryList("", "", lib.CategoryExercise, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + symptoms, _ := lib.EntryList("", "", lib.CategorySymptom, &lib.EntryFilter{DossierID: targetID, Limit: 1}) stats["vitals"] = len(vitals) stats["medications"] = len(meds) @@ -171,22 +171,22 @@ func BuildDossierSections(targetID, targetHex string, target *lib.Dossier, p *li section.Searchable = len(section.Items) > 5 case "documents": - entries, _ := lib.EntryList(nil, "", lib.CategoryDocument, &lib.EntryFilter{DossierID: targetID, Limit: 50}) + entries, _ := lib.EntryList("", "", lib.CategoryDocument, &lib.EntryFilter{DossierID: targetID, Limit: 50}) section.Items = entriesToSectionItems(entries) section.Summary = fmt.Sprintf("%d documents", len(entries)) case "procedures": - entries, _ := lib.EntryList(nil, "", lib.CategorySurgery, &lib.EntryFilter{DossierID: targetID, Limit: 50}) + entries, _ := lib.EntryList("", "", lib.CategorySurgery, &lib.EntryFilter{DossierID: targetID, Limit: 50}) section.Items = entriesToSectionItems(entries) section.Summary = fmt.Sprintf("%d procedures", len(entries)) case "assessments": - entries, _ := lib.EntryList(nil, "", lib.CategoryAssessment, &lib.EntryFilter{DossierID: targetID, Limit: 50}) + entries, _ := lib.EntryList("", "", lib.CategoryAssessment, &lib.EntryFilter{DossierID: targetID, Limit: 50}) section.Items = entriesToSectionItems(entries) section.Summary = fmt.Sprintf("%d assessments", len(entries)) case "genetics": - genomeEntries, _ := lib.EntryList(nil, "", lib.CategoryGenome, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + genomeEntries, _ := lib.EntryList("", "", lib.CategoryGenome, &lib.EntryFilter{DossierID: targetID, Limit: 1}) if len(genomeEntries) > 0 { section.Summary = "Loading..." } @@ -220,7 +220,7 @@ func BuildDossierSections(targetID, targetHex string, target *lib.Dossier, p *li default: // Generic handler for any category with a Category set if cfg.Category > 0 { - entries, _ := lib.EntryList(nil, "", cfg.Category, &lib.EntryFilter{DossierID: targetID, Limit: 50}) + entries, _ := lib.EntryList("", "", cfg.Category, &lib.EntryFilter{DossierID: targetID, Limit: 50}) section.Items = entriesToSectionItems(entries) // Use section ID for summary (e.g., "2 medications" not "2 items") section.Summary = fmt.Sprintf("%d %s", len(entries), cfg.ID) @@ -366,7 +366,7 @@ func buildLabItems(dossierID, lang string, T func(string) string) ([]SectionItem orders, _ := lib.EntryQuery(dossierID, lib.CategoryLab, "lab_order") // Also get standalone lab results (no parent) - allLabs, _ := lib.EntryList(nil, "", lib.CategoryLab, &lib.EntryFilter{DossierID: dossierID, Limit: 5000}) + allLabs, _ := lib.EntryList("", "", lib.CategoryLab, &lib.EntryFilter{DossierID: dossierID, Limit: 5000}) var standalones []*lib.Entry for _, e := range allLabs { if e.ParentID == "" && e.Type != "lab_order" { @@ -727,7 +727,7 @@ func handleDossierV2(w http.ResponseWriter, r *http.Request) { } // Check for genome - genomeEntries, _ := lib.EntryList(nil, "", lib.CategoryGenome, &lib.EntryFilter{DossierID: targetID, Limit: 1}) + genomeEntries, _ := lib.EntryList("", "", lib.CategoryGenome, &lib.EntryFilter{DossierID: targetID, Limit: 1}) hasGenome := len(genomeEntries) > 0 // Build sections diff --git a/portal/main.go b/portal/main.go index 64e4d8f..0a3bbff 100644 --- a/portal/main.go +++ b/portal/main.go @@ -913,7 +913,7 @@ func handleDemo(w http.ResponseWriter, r *http.Request) { for _, s := range studies { totalSlices += s.SliceCount } - genomeEntries, _ := lib.EntryList(nil, "", lib.CategoryGenome, &lib.EntryFilter{DossierID: demoDossierID, Limit: 1}) // nil ctx - demo lookup + genomeEntries, _ := lib.EntryList("", "", lib.CategoryGenome, &lib.EntryFilter{DossierID: demoDossierID, Limit: 1}) // nil ctx - demo lookup hasGenome := len(genomeEntries) > 0 // Build sections for demo dossier @@ -1443,8 +1443,7 @@ func handlePermissions(w http.ResponseWriter, r *http.Request) { if err != nil { http.NotFound(w, r); return } // Check manage permission - ctx := &lib.AccessContext{AccessorID: p.DossierID} - if !lib.CanManageDossier(ctx, targetID) { + if !lib.CanManageDossier(p.DossierID, targetID) { http.Error(w, "Forbidden: manage permission required", http.StatusForbidden) return } @@ -1612,8 +1611,7 @@ func handleEditAccess(w http.ResponseWriter, r *http.Request) { if err != nil { http.NotFound(w, r); return } // Check manage permission - ctx := &lib.AccessContext{AccessorID: p.DossierID} - if !lib.CanManageDossier(ctx, targetID) { + if !lib.CanManageDossier(p.DossierID, targetID) { http.Error(w, "Forbidden", http.StatusForbidden) return } diff --git a/portal/upload.go b/portal/upload.go index 39e3c90..b15cca7 100644 --- a/portal/upload.go +++ b/portal/upload.go @@ -48,7 +48,7 @@ func formatBytes(b int64) string { func getUploads(dossierID string) []Upload { var uploads []Upload - entries, err := lib.EntryList(nil, "", lib.CategoryUpload, &lib.EntryFilter{ // nil ctx - internal operation + entries, err := lib.EntryList("", "", lib.CategoryUpload, &lib.EntryFilter{ // nil ctx - internal operation DossierID: dossierID, Limit: 50, }) @@ -104,7 +104,7 @@ func getUploadEntry(entryID, dossierID string) (filePath, fileName, category, st // findUploadByFilename finds existing uploads with the same filename func findUploadByFilename(dossierID, filename string) []*lib.Entry { - entries, err := lib.EntryList(nil, "", lib.CategoryUpload, &lib.EntryFilter{ // nil ctx - internal operation + entries, err := lib.EntryList("", "", lib.CategoryUpload, &lib.EntryFilter{ // nil ctx - internal operation DossierID: dossierID, Value: filename, }) @@ -399,7 +399,7 @@ func handleProcessImaging(w http.ResponseWriter, r *http.Request) { log("Access OK") // Get all uploads with status=uploaded (any type for now) - entries, err := lib.EntryList(nil, "", lib.CategoryUpload, &lib.EntryFilter{ + entries, err := lib.EntryList("", "", lib.CategoryUpload, &lib.EntryFilter{ DossierID: targetID, }) if err != nil {