[Generated on 2025-01-05 by Claude Sonnet 4.5 with access to the Gramps Web API repsotitory.]
Role-Based Access Control (RBAC):
- 6 user roles: Guest (0), Member (1), Contributor (2), Editor (3), Owner (4), Admin (5)
- Roles grant hierarchical permissions with each level inheriting from the previous
- Permissions stored in JWT claims for stateless authorization
Privacy Control:
PERM_VIEW_PRIVATE: Enables/disables viewing of Gramps objects marked private- When disabled, users get a
ModifiedPrivateProxyDbwrapping the database - Based on Gramps' native
PrivateProxyDbwhich filters private records
Object-Level Permissions:
PERM_ADD_OBJ: Add new genealogical objects (Contributor+)PERM_EDIT_OBJ: Edit existing objects (Editor+)PERM_DEL_OBJ: Delete objects (Editor+)
Current Limitations:
- Binary privacy: either see all private records or none
- No per-object-type permissions (e.g., edit people but not sources)
- No relationship-based access (e.g., view only close relatives)
- No branch-specific editing rights
- Relationship-Based Viewing: View details for people within N generations
- Branch-Specific Editing: Edit only a specific lineage/branch
- Graduated Privacy Levels: More than just public/private
- Context-Aware Access: Different rights for different parts of the tree
Approach: Build custom proxy classes extending Gramps' existing proxy system
Implementation:
- Create
RelationshipProxyDbwrapping the base database - Filter objects based on relationship calculations at query time
- Similar to existing
PrivateProxyDbbut with genealogical logic
Pros:
- Consistent with current architecture
- Leverages proven Gramps proxy patterns
- Minimal changes to API endpoints
- Database-agnostic (works with all filters/exports)
Cons:
- Performance overhead for complex relationship calculations
- Relationship computation on every database access
- Limited flexibility for non-relationship-based rules
Complexity: Medium Performance Impact: Medium-High (relationship calculations are expensive)
Approach: Store explicit user-object permissions in a database table
Implementation:
- New
user_object_permissionstable with columns: user_id, object_handle, object_type, permission_level - Pre-compute and cache accessible objects per user
- Check permissions in middleware before returning data
Pros:
- Fast lookups (indexed database queries)
- Maximum flexibility (any permission pattern)
- Can handle complex rules once computed
- Explicit audit trail
Cons:
- Significant storage overhead (potentially millions of rows)
- Complex maintenance (updates when objects added/modified)
- Relationship changes require permission recalculation
- Requires schema migrations
Complexity: High Performance Impact: Low (after initial computation)
Approach: Store Gramps filter definitions per user role/permission level
Implementation:
- Extend user table with
viewing_filterandediting_filterJSON columns - Filters use Gramps' native filter engine (already supports relationship rules)
- Apply user's filter when calling
get_db_handle() - Reuse existing filter infrastructure (ancestors, descendants, etc.)
Pros:
- Leverages existing, well-tested Gramps filter system
- Filters already support relationship-based rules
- User-configurable without code changes
- Works with existing
FilterProxyDbinfrastructure
Cons:
- Filter complexity can be hard for admins to configure
- Some performance impact (filter evaluation at query time)
- Limited by Gramps filter capabilities
Complexity: Medium Performance Impact: Medium
Approach: Define permissions based on attributes of user, object, and context
Implementation:
- Policy engine evaluating rules like:
- "User can view Person if relationship_distance <= user.max_viewing_distance"
- "User can edit Person if person.handle in user.editable_branch_roots + descendants"
- Policies stored as evaluatable expressions (e.g., Python expressions or policy DSL)
Pros:
- Most flexible and powerful approach
- Supports complex conditional logic
- Industry-standard pattern (similar to AWS IAM policies)
- Can combine multiple criteria
Cons:
- Most complex to implement
- Significant learning curve for administrators
- Policy language/engine required
- Potential security risks if not carefully implemented
Complexity: Very High Performance Impact: Medium
This combines the strengths of the existing architecture with proven Gramps functionality.
Challenge: When filtering People, what happens to associated objects?
Example Scenarios:
- Person passes filter → should their birth event be visible?
- Person fails filter → can you still see an event where they're a witness?
- Can you view a note attached to a filtered-out person?
Solution: Transitive Inclusion with ReferencedBySelectionProxyDb
Gramps already provides ReferencedBySelectionProxyDb which:
- Takes a set of "approved" object handles
- Includes ALL objects referenced by those approved objects
- Used in exports with person filters
Implementation Strategy:
class FilteredProxyDb(ProxyDbBase):
def __init__(self, db, person_filter):
super().__init__(db)
# Apply person filter to get approved people
approved_people = apply_person_filter(db, person_filter)
# Use ReferencedBySelectionProxyDb for transitive inclusion
self._proxy = ReferencedBySelectionProxyDb(
db,
person_list=approved_people,
note_list=[], # Will be auto-included if referenced
event_list=[],
source_list=[],
# ...
)
# Delegate all methods to self._proxyAccess Rules:
- People: Only those matching the filter
- Events: Included if referenced by filtered people (birth, death, etc.)
- Families: Included if either parent matches filter
- Notes: Included if attached to any included object
- Sources/Citations: Included if cited by any included object
- Media: Included if referenced by any included object
- Places: Included if used in any included event
Edge Cases:
| Scenario | Behavior |
|---|---|
| Person A matches filter, married to Person B who doesn't | Family visible, B appears as spouse but B's details hidden when accessing directly |
| Event has multiple participants, only some match filter | Event visible, but participant list filtered |
| Note shared between filtered and non-filtered people | Note visible (conservative approach) |
Configuration Options:
For maximum flexibility, add a policy setting:
# In user configuration
related_objects_policy: str = "transitive" # or "strict"- "transitive" (recommended): Include all objects referenced by filtered people
- "strict": Also filter related objects (e.g., only show events for filtered people)
Why Transitive is Better:
- Prevents broken references in UI
- Maintains data integrity and context
- Matches user expectations (if I can see a person, I expect to see their birth event)
- Consistent with Gramps export behavior
-
Extend User Model
class User: viewing_filter_json: str # Serialized Gramps filter editing_filter_json: str # For write permissions privacy_level: int # 0=all, 1=non-private, 2=custom related_objects_policy: str # "transitive" (default) or "strict"
-
Create
FilteredProxyDbClass- Extends
ModifiedPrivateProxyDb - Applies user's viewing filter to People
- Uses
ReferencedBySelectionProxyDbfor transitive inclusion - Caches relationship calculations per request
- Extends
-
Update
get_db_handle()Logicdef get_db_handle(readonly=True): claims = get_jwt() if claims.get('viewing_filter'): filter_def = claims['viewing_filter'] return FilteredProxyDb(base_db, filter_def) elif not claims.get(PERM_VIEW_PRIVATE): return ModifiedPrivateProxyDb(base_db) return base_db
-
Filter Builder UI for admins
- Preset templates: "Descendants of X within N generations"
- Visual filter editor leveraging existing Gramps UI patterns
-
User Management Extensions
- Assign filters to users/roles
- "Anchor person" field (which person is the user in the tree?)
-
API Endpoints
PUT /users/{username}/viewing-filterGET /users/{username}/accessible-people(preview)
-
Multi-Level Privacy
- Extend privacy flag from boolean to 0-3 levels
- Filter by privacy level: "show level 0-1 only"
-
Branch-Specific Editing
- Separate
editing_filter_jsonfield - Apply to write operations (POST/PUT/DELETE)
- Validation: ensure user can view what they're editing
- Separate
-
Performance Optimization
- Cache relationship calculations in Redis
- Pre-compute common filter results
- Lazy-load filtered object lists
Options:
- A) JSON representation of Gramps filter rules ✓ RECOMMENDED
- B) Custom DSL
- C) Python code snippets
Rationale: JSON format allows validation, is language-agnostic for frontend, and maps directly to existing Gramps filter system.
Options:
- A) Always compute filters dynamically ✓ RECOMMENDED for MVP
- B) Pre-compute and cache filtered object lists
- C) Hybrid: cache for common filters, compute for custom
Rationale: Start with dynamic computation for correctness, add caching later based on profiling. Premature optimization here risks bugs.
Options:
- A) Maintain current privacy system, add filters alongside ✓ RECOMMENDED
- B) Migrate all users to filter-based system
- C) Parallel systems with toggle
Rationale: Don't break existing deployments. Null viewing_filter_json → use legacy PERM_VIEW_PRIVATE.
Options:
- A) Separate viewing and editing filters ✓ RECOMMENDED
- B) Same filter for both (edit implies view)
- C) Edit requires superset of view permissions
Rationale: Different use cases: users who can view extended family but only edit immediate family.
Requirement: Member can view people within 3 generations
Implementation:
{
"rules": [
{
"name": "IsLessThanNthGenerationAncestorOf",
"values": ["<USER_PERSON_ID>", "4"]
},
{
"name": "IsLessThanNthGenerationDescendantOf",
"values": ["<USER_PERSON_ID>", "4"]
}
],
"operator": "or"
}Admin workflow:
- Identify user's person record in tree
- Select "View close relatives" template
- Set generations = 3
- Save to user's
viewing_filter_json
Requirement: Contributor can edit descendants of their grandparent
Implementation:
{
"editing_filter_json": {
"rules": [
{
"name": "IsDescendantOf",
"values": ["<BRANCH_ROOT_ID>", "1"]
}
]
}
}Middleware check on POST/PUT/DELETE:
def require_edit_permission(handle, obj_type):
if has_permissions([PERM_EDIT_OBJ]):
# No restrictions for Editors+
return
user_id = get_jwt_identity()
editing_filter = get_user_editing_filter(user_id)
if not editing_filter:
abort(403)
if not object_matches_filter(handle, obj_type, editing_filter):
abort(403, "Object not in your editable branch")Requirement: Different privacy tiers (public → family → close family → private)
Implementation:
- Database: Add
privacy_levelinteger field to objects (0-3) - User setting:
max_privacy_level(Guest=0, Member=1, Contributor=2, Editor+=3) - Proxy filter: Filter objects where
obj.privacy_level <= user.max_privacy_level
Migration: Current boolean private → privacy_level = 3 if private else 0
-
JWT Token Size: Embedding full filters in JWT could exceed size limits
- Solution: Store filter ID in JWT, fetch full filter server-side
-
Filter Injection: Malicious filter JSON could compromise security
- Solution: Validate filter JSON against schema before saving
- Solution: Use read-only filter evaluation (no dynamic code execution)
-
Performance DoS: Complex filters could overload server
- Solution: Set max filter complexity (e.g., max 10 rules)
- Solution: Timeout filter evaluation after N seconds
-
Write Authorization: Ensure edit filters are properly enforced
- Solution: Double-check permissions in write operations (defense in depth)
- Solution: Audit log all permission changes
-
Information Leakage via Related Objects
- Risk: User accesses filtered-out person via shared note/event
- Solution: Transitive policy prevents direct access, only shows object in context
- Solution: API endpoints check object type and enforce primary filter
-
Family Relationship Exposure
- Risk: Seeing family record reveals existence of filtered-out spouse
- Solution: Proxy database can redact spouse details in family objects
- Alternative: Family only visible if both spouses pass filter (stricter)
Phase 1 - Preparation:
- Database migration adds new columns (default NULL)
- Existing behavior unchanged (NULL filters = use legacy permissions)
Phase 2 - Opt-In:
- Admins can assign filters to specific users
- Both systems work in parallel
Phase 3 - Full Migration (Optional):
- Convert
PERM_VIEW_PRIVATEto filter equivalent - Deprecate legacy permission flags
Based on typical Gramps database with 10,000 people:
| Operation | Current | Filter-Based (uncached) | With Caching |
|---|---|---|---|
| Get person list | 50ms | 150ms | 60ms |
| Get single person | 5ms | 8ms | 5ms |
| Relationship calc | N/A | 200ms (initial) | 10ms |
| Search people | 100ms | 200ms | 110ms |
Mitigation strategies:
- Cache relationship graphs per session
- Use lazy evaluation (only filter when accessing)
- Index commonly used relationship paths
If full implementation is too complex, consider these incremental improvements:
- Add
PERM_VIEW_LIVINGpermission - Use Gramps' existing
LivingProxyDb - Restricts viewing to deceased only
Effort: 1 day Benefit: Privacy for living relatives
- Add enum:
viewing_scope: "all" | "non-private" | "living-only" | "deceased-only" - Map to existing proxy classes
- No custom filters, just fixed options
Effort: 3 days Benefit: Simple multi-level access without complexity
- Add
home_person_handleto User model - New permission
PERM_VIEW_EXTENDED_FAMILY - If set, only show people within N degrees
Effort: 1 week Benefit: Relationship-based viewing without full filter system
Before proceeding with implementation, please clarify:
-
Priority Use Cases: Which is most important?
- Viewing restrictions (relationship-based)?
- Editing restrictions (branch-based)?
- Privacy levels (graduated access)?
-
User Base: How many users per tree typically?
- Affects caching strategy and storage considerations
-
Performance Requirements: What's acceptable latency for filtered queries?
- Helps choose between caching strategies
-
Administrative Complexity: Who will manage these permissions?
- Affects UI complexity requirements
-
Backward Compatibility: Must all existing installations work unchanged?
- Determines migration approach
-
Timeline: What's the target timeframe for MVP?
- Helps scope initial implementation
-
Related Objects Policy: For filtered people, should associated objects (events, notes, sources) be:
- A) Always included (transitive - recommended for usability)
- B) Also filtered (stricter privacy, but may break context)
- C) Configurable per user/role?
-
Information Disclosure Tolerance: Is it acceptable for users to:
- Know filtered-out people exist (via family relationships)?
- See event/note titles but not details for filtered objects?
- Or should filtered objects be completely invisible?
The Filter-Based Permissions (Option 3) approach offers the best balance of:
- Leveraging existing Gramps infrastructure
- Flexibility for various use cases
- Reasonable implementation complexity
- Good performance with caching
Starting with the "Quick Win" solutions can provide immediate value while the full system is developed.
Recommended Next Steps:
- Review and discuss this document
- Prioritize use cases
- Implement one "Quick Win" for immediate improvement
- Begin Phase 1 of full filter-based system
- Iterate based on user feedback
Hi David,
First, thank you for the incredible work on Gramps Web.
I recently read your Gist exploring advanced filter-based permissions (Option 3). I’d like to propose an architectural alternative that builds upon your ideas—specifically your mention of using
ReferencedBySelectionProxyDb—but shifts the computational load away from the SQL database to ensure high performance.The concept is a Hybrid ReBAC + ABAC model utilizing string-based entitlements (similar to AWS IAM policies) and an in-memory graph.
Here is how it would work:
1. String-Based Entitlements (AWS ARN style)
Instead of complex JSON filters or recursive SQL queries, permissions are defined as strings using a standard funnel approach (Macro to Micro), adding explicit Allow/Deny rules and Condition filters.
Schema:
EFFECT:DATASET:ACTION:ROOT_HANDLE:DIRECTION:DEPTH:CONDITIONEFFECT:allowordenyCONDITION(optional):alive,deceased,private,public, or*These strings are stored natively in the Gramps Web database (keeping the system standalone), could be grouped in roles, and can easily be mapped via OIDC custom claims for users with external IdPs.
Here are some practical examples:
allow:*:r:*:*:*:*allow:MyTree:r:*:*:*:deceasedallow:MyTree:r:*:*:*:*deny:MyTree:*:*:*:*:privateallow:MyTree:rw:I0142:DESC:*:*allow:MyTree:r:current:ASC:*:*2. Standard Templates for New Trees
Gramps Web could propose default roles when a new Tree is created, automatically generating these baseline entitlements:
allow:{Dataset}:admin:*:*:*:*allow:{Dataset}:rw:*:*:*:*allow:{Dataset}:r:*:*:*:publicallow:{Dataset}:r:current:ASC:*:*allow:{Dataset}:r:current:DESC:*:*allow:{Dataset}:r:current:*:0:*3. In-Memory Topology via
networkx(The PDP, Policy Decision Point)Running recursive SQL queries on every HTTP request will inevitably cause performance bottlenecks. Instead, upon loading a Dataset, we extract only the bare topology (nodes = Handles, edges = Parent/Child relationships) into a single, shared Python
networkxdirected graph in RAM.Crucially, we also append lightweight node attributes during initialization (e.g.,
G.add_node("I0001", alive=True, private=False)).Because this topology represents the static resource, the memory footprint is negligible. An entire tree of 100,000 individuals with basic boolean attributes takes less than 50MB. Graph traversals to compute the allowed node list take less than a millisecond.
4. Fail-Fast Middleware (The PEP, Policy Enforcement Point) & Data Leakage Prevention
A proxy intercepts API requests and applies the policies:
DATASETdoesn't match or if an action is insufficient.networkxgraph based on the user's strings, computing the allow list of Person Handles, subtracting any deny matches, and filtering out nodes that don't match theCONDITION(like deceased).ReferencedBySelectionProxyDb.By doing so, the native proxy automatically filters out all Media, Notes, and Events that do not belong to the authorized sub-graph or belong to a denied/living person.
This architecture keeps the database schema simple, resolves the GDPR/living persons issue elegantly, guarantees lightning-fast API responses, and natively solves the data leakage problem.
I would love to hear your thoughts on this approach and whether you think this would be a viable direction for a PoC.
Best regards,