package lib import ( "fmt" "strings" "time" ) // Permission constants (bitmask) const ( PermRead = 1 // Read access PermWrite = 2 // Create/update PermDelete = 4 // Delete PermManage = 8 // Grant/revoke access to others ) // CheckAccess checks if accessor has permission to access an entry/category/dossier // Returns true if access is granted, false otherwise func CheckAccess(accessorID, dossierID, entryID string, perm int) bool { // Owner always has full access if accessorID == dossierID { return true } // Query all grants for this accessor on this dossier var grants []Access if err := dbQuery( "SELECT access_id, dossier_id, grantee_id, entry_id, ops FROM access WHERE grantee_id = ? AND dossier_id = ?", []any{accessorID, dossierID}, &grants, ); err != nil { return false } // Check grants in order of specificity: // 1. Exact entry match // 2. Category match (entry's parent matches grant) // 3. Dossier root match (grant.entry_id == dossierID) for _, grant := range grants { // Exact entry match if grant.EntryID == entryID { return (grant.Ops & perm) != 0 } // Dossier root match (full access) if grant.EntryID == dossierID { return (grant.Ops & perm) != 0 } // Category match - need to load entry to check parent if entryID != dossierID && entryID != "" { var entry Entry if err := dbLoad("entries", entryID, &entry); err == nil { if entry.ParentID == grant.EntryID || entry.ParentID == "" && grant.EntryID == dossierID { return (grant.Ops & perm) != 0 } } } } return false } // GrantAccess grants permission to access an entry/category/dossier func GrantAccess(dossierID, granteeID, entryID string, ops int) error { access := &Access{ AccessID: NewID(), DossierID: dossierID, GranteeID: granteeID, EntryID: entryID, Ops: ops, CreatedAt: time.Now().Unix(), } if err := dbSave("access", access); err != nil { return err } // Audit log details := fmt.Sprintf("Granted %s access to entry %s (ops=%d)", granteeID, entryID, ops) AuditLog(dossierID, "grant_access", granteeID, details) return nil } // RevokeAccess revokes permission for a specific entry/category/dossier func RevokeAccess(dossierID, granteeID, entryID string) error { // Find the access record var grants []Access if err := dbQuery( "SELECT access_id FROM access WHERE dossier_id = ? AND grantee_id = ? AND entry_id = ?", []any{dossierID, granteeID, entryID}, &grants, ); err != nil { return err } for _, grant := range grants { if err := dbDelete("access", "access_id", grant.AccessID); err != nil { return err } } // Audit log details := fmt.Sprintf("Revoked %s access to entry %s", granteeID, entryID) AuditLog(dossierID, "revoke_access", granteeID, details) return nil } // RevokeAllAccess revokes all permissions for a grantee on a dossier func RevokeAllAccess(dossierID, granteeID string) error { var grants []Access if err := dbQuery( "SELECT access_id FROM access WHERE dossier_id = ? AND grantee_id = ?", []any{dossierID, granteeID}, &grants, ); err != nil { return err } for _, grant := range grants { if err := dbDelete("access", "access_id", grant.AccessID); err != nil { return err } } // Audit log details := fmt.Sprintf("Revoked all %s access to dossier", granteeID) AuditLog(dossierID, "revoke_all_access", granteeID, details) return nil } // ListGrants returns all grants for a specific grantee on a dossier func ListGrants(dossierID, granteeID string) ([]*Access, error) { var grants []*Access return grants, dbQuery( "SELECT * FROM access WHERE dossier_id = ? AND grantee_id = ? ORDER BY created_at DESC", []any{dossierID, granteeID}, &grants, ) } // ListGrantees returns all grantees who have access to a dossier func ListGrantees(dossierID string) ([]*Access, error) { var grants []*Access return grants, dbQuery( "SELECT * FROM access WHERE dossier_id = ? ORDER BY grantee_id, created_at DESC", []any{dossierID}, &grants, ) } // ListAccessibleCategories returns list of category integers the accessor can see for this dossier func ListAccessibleCategories(accessorID, dossierID string) ([]int, error) { // Owner sees all categories if accessorID == dossierID { return []int{ CategoryImaging, CategoryDocument, CategoryLab, CategoryGenome, CategoryConsultation, CategoryDiagnosis, CategoryVital, CategoryExercise, CategoryMedication, CategorySupplement, CategoryNutrition, CategoryFertility, CategorySymptom, CategoryNote, CategoryHistory, CategoryFamilyHistory, CategorySurgery, CategoryHospital, CategoryBirth, CategoryDevice, CategoryTherapy, CategoryAssessment, CategoryProvider, CategoryQuestion, }, nil } // Get all grants for this accessor var grants []Access if err := dbQuery( "SELECT entry_id FROM access WHERE grantee_id = ? AND dossier_id = ?", []any{accessorID, dossierID}, &grants, ); err != nil { return nil, err } // If any grant is for the dossier root, return all categories for _, grant := range grants { if grant.EntryID == dossierID { return []int{ CategoryImaging, CategoryDocument, CategoryLab, CategoryGenome, CategoryConsultation, CategoryDiagnosis, CategoryVital, CategoryExercise, CategoryMedication, CategorySupplement, CategoryNutrition, CategoryFertility, CategorySymptom, CategoryNote, CategoryHistory, CategoryFamilyHistory, CategorySurgery, CategoryHospital, CategoryBirth, CategoryDevice, CategoryTherapy, CategoryAssessment, CategoryProvider, CategoryQuestion, }, nil } } // Otherwise, load each entry and collect unique categories categoryMap := make(map[int]bool) for _, grant := range grants { if grant.EntryID == "" { continue } var entry Entry if err := dbLoad("entries", grant.EntryID, &entry); err == nil { categoryMap[entry.Category] = true } } categories := make([]int, 0, len(categoryMap)) for cat := range categoryMap { categories = append(categories, cat) } return categories, nil } // ============================================================================ // DEPRECATED - Legacy compatibility, will be removed // ============================================================================ // AccessContext - DEPRECATED, for backward compatibility only type AccessContext struct { DossierID string AccessorID string IsSystem bool } // SystemContext - DEPRECATED, for backward compatibility only var SystemContext = &AccessContext{DossierID: "system", AccessorID: "system", IsSystem: true} // checkAccess - DEPRECATED wrapper for old signature // Old signature: checkAccess(accessorID, dossierID, entryID string, category int, perm rune) func checkAccess(accessorID, dossierID, entryID string, category int, perm rune) error { // Convert rune permission to int var permInt int switch perm { case 'r': permInt = PermRead case 'w': permInt = PermWrite case 'd': permInt = PermDelete case 'm': permInt = PermManage default: return fmt.Errorf("invalid permission: %c", perm) } // If entryID is empty, use dossierID (root access check) if entryID == "" { entryID = dossierID } if CheckAccess(accessorID, dossierID, entryID, permInt) { return nil } return fmt.Errorf("access denied") } // InvalidateCacheForAccessor - DEPRECATED no-op (no caching in new RBAC) func InvalidateCacheForAccessor(accessorID string) {} // EnsureCategoryRoot - DEPRECATED stub func EnsureCategoryRoot(dossierID string, category int) (string, error) { return dossierID, nil } // mergeOps - DEPRECATED stub func mergeOps(a, b int) int { return a | b } // OpsToString converts ops bitmask to string representation func OpsToString(ops int) string { var parts []string if ops&PermRead != 0 { parts = append(parts, "r") } if ops&PermWrite != 0 { parts = append(parts, "w") } if ops&PermDelete != 0 { parts = append(parts, "d") } if ops&PermManage != 0 { parts = append(parts, "m") } return strings.Join(parts, "") } // ErrAccessDenied - DEPRECATED error for backward compatibility var ErrAccessDenied = fmt.Errorf("access denied") // CanManageDossier - DEPRECATED wrapper func CanManageDossier(accessorID, dossierID string) bool { return CheckAccess(accessorID, dossierID, dossierID, PermManage) }