293 lines
8.1 KiB
Go
293 lines
8.1 KiB
Go
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)
|
|
}
|