inou/lib/roles.go

246 lines
6.4 KiB
Go

package lib
// ============================================================================
// System Role Definitions
// ============================================================================
// Predefined roles that can be applied to grantees. Each role defines
// a set of operations (r/w/d/m) at root level and optionally at specific
// categories.
// ============================================================================
// RoleGrant defines a single permission grant within a role
type RoleGrant struct {
Category int // 0 = root (all categories), >0 = specific category
Ops string // "r", "rw", "rwd", "rwdm"
}
// RoleTemplate defines a predefined role with its grants
type RoleTemplate struct {
Name string // Role identifier (e.g., "Family", "Doctor")
Description string // Human-readable description
Grants []RoleGrant // Permission grants
}
// SystemRoles defines all available role templates
var SystemRoles = []RoleTemplate{
{
Name: "Family",
Description: "Full access for family members",
Grants: []RoleGrant{
{Category: 0, Ops: "rwdm"}, // Full access to everything
},
},
{
Name: "Doctor",
Description: "Read/write access for healthcare providers",
Grants: []RoleGrant{
{Category: 0, Ops: "rw"}, // Read/write to everything
},
},
{
Name: "Caregiver",
Description: "Read/write access for caregivers",
Grants: []RoleGrant{
{Category: 0, Ops: "rw"}, // Read/write to everything
},
},
{
Name: "Trainer",
Description: "Read-only with write access to exercise and nutrition",
Grants: []RoleGrant{
{Category: 0, Ops: "r"}, // Read everything
{Category: CategoryExercise, Ops: "rw"}, // Write exercise
{Category: CategoryNutrition, Ops: "rw"}, // Write nutrition
},
},
{
Name: "Friend",
Description: "Read-only access",
Grants: []RoleGrant{
{Category: 0, Ops: "r"}, // Read everything
},
},
{
Name: "Researcher",
Description: "Read-only access for research purposes",
Grants: []RoleGrant{
{Category: 0, Ops: "r"}, // Read everything
},
},
}
// GetSystemRoles returns all system role templates
func GetSystemRoles() []RoleTemplate {
return SystemRoles
}
// GetSystemRole returns a specific role template by name
func GetSystemRole(name string) *RoleTemplate {
for _, r := range SystemRoles {
if r.Name == name {
return &r
}
}
return nil
}
// ApplyRoleTemplate creates access grants for a grantee based on a role template.
// For category-specific grants, creates a root entry for that category in the
// dossier to serve as the grant target.
func ApplyRoleTemplate(dossierID, granteeID, roleName string) error {
role := GetSystemRole(roleName)
if role == nil {
// Unknown role - create basic read-only grant
return AccessGrantWrite(&Access{
DossierID: dossierID,
GranteeID: granteeID,
EntryID: "", // root
Role: roleName,
Ops: "r",
})
}
var grants []*Access
for _, g := range role.Grants {
grant := &Access{
DossierID: dossierID,
GranteeID: granteeID,
Role: roleName,
Ops: g.Ops,
}
if g.Category == 0 {
// Root grant
grant.EntryID = ""
} else {
// Category-specific grant
// Find or create category root entry
catRootID, err := findOrCreateCategoryRoot(dossierID, g.Category)
if err != nil {
return err
}
grant.EntryID = catRootID
}
grants = append(grants, grant)
}
err := AccessGrantWrite(grants...)
if err != nil {
return err
}
// Invalidate cache for grantee
InvalidateCacheForAccessor(granteeID)
return nil
}
// 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", use empty string for system context)
entries, err := EntryList(SystemAccessorID, "", category, &EntryFilter{
DossierID: dossierID,
Type: "category_root",
Limit: 1,
})
if err == nil && len(entries) > 0 {
return entries[0].EntryID, nil
}
// Create virtual category root entry
entry := &Entry{
DossierID: dossierID,
Category: category,
Type: "category_root",
Value: CategoryName(category),
}
if err := EntryWrite(nil, entry); err != nil {
return "", err
}
return entry.EntryID, nil
}
// RevokeRole removes all grants with the specified role for a grantee on a dossier
func RevokeRole(dossierID, granteeID, roleName string) error {
grants, err := AccessGrantList(&PermissionFilter{
DossierID: dossierID,
GranteeID: granteeID,
Role: roleName,
})
if err != nil {
return err
}
var ids []string
for _, g := range grants {
ids = append(ids, g.AccessID)
}
if len(ids) > 0 {
if err := AccessGrantRemove(ids...); err != nil {
return err
}
}
// Invalidate cache for grantee
InvalidateCacheForAccessor(granteeID)
return nil
}
// GetGranteeRole returns the primary role name for a grantee on a dossier
// If multiple roles exist, returns the first one found
func GetGranteeRole(dossierID, granteeID string) string {
grants, err := AccessGrantList(&PermissionFilter{
DossierID: dossierID,
GranteeID: granteeID,
})
if err != nil || len(grants) == 0 {
return ""
}
return grants[0].Role
}
// GetGranteesWithAccess returns all grantees with any access to a dossier
// along with their role and ops
type GranteeSummary struct {
GranteeID string
Name string // Resolved from dossier
Role string
Ops string // Combined ops from all grants
}
func GetGranteesWithAccess(dossierID string) ([]GranteeSummary, error) {
grants, err := AccessGrantList(&PermissionFilter{DossierID: dossierID})
if err != nil {
return nil, err
}
// Group by grantee
byGrantee := make(map[string]*GranteeSummary)
for _, g := range grants {
if g.GranteeID == "" {
continue // Skip templates
}
if byGrantee[g.GranteeID] == nil {
byGrantee[g.GranteeID] = &GranteeSummary{
GranteeID: g.GranteeID,
Role: g.Role,
}
}
// Merge ops
byGrantee[g.GranteeID].Ops = mergeOps(byGrantee[g.GranteeID].Ops, g.Ops)
}
// Resolve names (using nil ctx for internal operation)
var result []GranteeSummary
for _, gs := range byGrantee {
if d, err := DossierGet(nil, gs.GranteeID); err == nil {
gs.Name = d.Name
}
result = append(result, *gs)
}
return result, nil
}