Last updated

ACL and User Management

Papr Memory provides enterprise-grade security through a comprehensive Access Control List (ACL) system and robust multi-tenancy support. This page explains how to manage access control and implement multi-tenant isolation.

User Management and Access Control

Papr Memory integrates user management with its access control system, allowing you to create users and manage memories for specific users within your security framework.

Creating and Managing Users

# Create a new user
user = client.user.create(
    external_id="user123",
    email="user@example.com",
    metadata={
        "name": "John Doe",
        "role": "developer",
        "team": "Engineering"
    },
    type="developerUser"  # Options: "developerUser", "user", "agent"
)

# Retrieve user details
user_details = client.user.get(user.user_id)

# Update user information
updated_user = client.user.update(
    user_id=user.user_id,
    email="updated.user@example.com",
    metadata={
        "name": "John Doe",
        "role": "senior developer",
        "team": "Engineering"
    }
)

# Delete a user if needed
delete_response = client.user.delete(user.user_id)

# Bulk create users for improved performance
bulk_users = client.user.create_batch(
    users=[
        {"external_id": "user123", "email": "user123@example.com"},
        {"external_id": "user456", "email": "user456@example.com"},
        {"external_id": "user789", "email": "user789@example.com"}
    ]
)

Using External User IDs

Papr Memory supports working with external user IDs directly without requiring explicit user creation. This simplifies onboarding and integration with your existing user management systems.

# Add memory with external user ID (user will be created automatically)
memory = client.memory.add(
    content="Customer feedback on feature X",
    type="text",
    metadata={
        "external_user_id": "customer_12345",  # No need to create user first
        "topics": "feedback, feature X",
        "hierarchical_structures": "Feedback/Features"
    }
)

# Batch add memories using external user ID
batch_response = client.memory.add_batch(
    external_user_id="customer_12345",  # External ID as top-level parameter
    memories=[
        {
            "content": "First interaction with customer",
            "type": "text",
            "metadata": {"topics": "onboarding, first contact"}
        },
        {
            "content": "Follow-up meeting notes",
            "type": "text",
            "metadata": {"topics": "meeting, follow-up"}
        }
    ]
)

# Search memories by external user ID
search_results = client.memory.search(
    query="Find customer feedback",
    external_user_id="customer_12345"  # Filter by external ID
)

How External User ID Processing Works

  1. When using an external user ID:

    • The system checks if a user with this external ID already exists
    • If not found, a new user is automatically created in the background
    • Memory items are associated with this resolved internal user
  2. Benefits of this approach:

    • Simplified onboarding - no separate user creation required
    • Seamless integration with existing user management systems
    • Reduced API calls for common workflows
  3. When to use explicit user creation vs. automatic creation:

    • Use explicit creation when you need to set specific user metadata or types
    • Use automatic creation for simpler workflows or rapid prototyping
    • Both approaches can coexist in the same application

Performance Optimization: Pre-creating Users

For applications with many users or high transaction volumes, pre-creating users in bulk can significantly improve performance:

# Pre-create users in bulk before they start using the system
bulk_users = client.user.create_batch(
    users=[
        {"external_id": "customer_1", "metadata": {"plan": "enterprise"}},
        {"external_id": "customer_2", "metadata": {"plan": "pro"}},
        {"external_id": "customer_3", "metadata": {"plan": "free"}},
        # ... hundreds or thousands more users
    ]
)

Benefits of pre-creating users:

  • Eliminates the overhead of creating users on-the-fly during memory operations
  • Reduces latency for first-time users' memory operations
  • Allows you to set up user metadata, permissions, and configurations in advance
  • Enables batch processing of user creation instead of individual API calls
  • Ensures consistent user settings across your application

When to consider pre-creating users:

  • When onboarding large numbers of users from an existing system
  • For applications with high transaction volumes
  • When user metadata and permissions need to be established before first use
  • In enterprise environments where user provisioning is a separate process

You can still use external_user_id directly in all memory operations even after pre-creating users.

Managing User-Specific Memories with Access Control

Access control is implemented through memory metadata fields that specify which users, workspaces, or roles have read or write access.

# Add memory with specific user permissions
memory = client.memory.add(
    content="Confidential project notes",
    type="text",
    metadata={
        "external_user_id": "user123",
        "topics": "project, confidential, planning",
        "user_read_access": ["user123", "admin_user"],
        "user_write_access": ["user123"]
    }
)

# Search memories with user context
results = client.memory.search(
    query="Find confidential project notes",
    external_user_id="user123"  # Filter by external ID
)

Access Control with External User IDs

You can also use external user IDs in your access control lists:

# Create memory with external user IDs in access control lists
shared_memory = client.memory.add(
    content="Project plan for Team Alpha",
    type="text",
    metadata={
        "external_user_id": "manager_1234",  # Owner by external ID
        "external_user_read_access": ["manager_1234", "engineer_5678", "designer_9012"],
        "external_user_write_access": ["manager_1234"]
    }
)

User-Specific Batch Operations

# Batch add memories for a specific user
response = client.memory.add_batch(
    memories=[
        {
            "content": "Meeting notes from team standup",
            "metadata": {"user_id": user.user_id, "topics": "meeting, standup"}
        },
        {
            "content": "Action items from planning session",
            "metadata": {"user_id": user.user_id, "topics": "planning, tasks"}
        }
    ]
)

Sharing Memories Between Users

Papr Memory allows sharing memories between users within a workspace or organization. This can be achieved in several ways:

Public vs Private Memories

By default, memories can be scoped by:

  1. Private to a user: Only accessible to the specific user
  2. Shared with specific users: Accessible to a defined list of users
  3. Available to a workspace: Accessible to all users in a workspace
  4. Organization-wide: Available across the entire organization
# Create a private memory (only for user_123)
private_memory = client.memory.add(
    content="My personal notes",
    type="text",
    metadata={
        "user_id": "user_123",
        "user_read_access": ["user_123"],
        "user_write_access": ["user_123"]
    }
)

# Create a memory shared with specific users
shared_memory = client.memory.add(
    content="Project plan for Team Alpha",
    type="text",
    metadata={
        "user_id": "user_123",
        "user_read_access": ["user_123", "user_456", "user_789"],
        "user_write_access": ["user_123", "user_456"]
    }
)

# Create a workspace-accessible memory
workspace_memory = client.memory.add(
    content="Company-wide announcement",
    type="text",
    metadata={
        "user_id": "user_123",
        "workspace_id": "workspace_001",
        "workspace_read_access": ["workspace_001"]
    }
)

# Create an organization-wide memory
org_memory = client.memory.add(
    content="Quarterly results",
    type="text",
    metadata={
        "user_id": "user_123",
        "role_read_access": ["all_employees"]  # Using a role that all employees have
    }
)

Updating Memory Sharing Settings

You can change the sharing settings of a memory by updating its metadata:

# Update memory to add additional user access
updated_memory = client.memory.update(
    memory_id="mem_123",
    metadata={
        "user_read_access": ["user_123", "user_456", "user_789", "user_new"],
        "user_write_access": ["user_123", "user_new"]
    }
)

# Change a private memory to be workspace-accessible
public_memory = client.memory.update(
    memory_id="mem_private",
    metadata={
        "workspace_read_access": ["workspace_001"],
        "role_read_access": ["manager", "developer"]
    }
)

Searching Shared Memories

When searching, you can access all memories shared with you, including those created by other users:

# Search accessible memories including those shared by others
shared_results = client.memory.search(
    query="Project plan",
    # No user_id filter means search all accessible memories
)

# Search within a specific workspace
workspace_results = client.memory.search(
    query="Company announcement",
    metadata={
        "workspace_id": "workspace_001"
    }
)

Workspace-Based Multi-Tenancy

In the Papr API, multi-tenancy is implemented primarily through the workspace concept. Each memory can be associated with a workspace, and access controls can be applied at the workspace level.

Access Control via Metadata

Access control is implemented through metadata fields in the memory objects:

{
  "metadata": {
    "user_id": "user_123",
    "external_user_id": "customer_456",
    "workspace_id": "workspace_789",
    "user_read_access": ["user_123", "user_456"],
    "user_write_access": ["user_123"],
    "external_user_read_access": ["customer_456", "customer_789"],
    "external_user_write_access": ["customer_456"],
    "workspace_read_access": ["workspace_789"],
    "workspace_write_access": [],
    "role_read_access": ["admin", "developer"],
    "role_write_access": ["admin"]
  }
}

Access Control Lists (ACL)

Permission Levels Through Metadata

  1. User-Level Permissions

    {
      "metadata": {
        "user_read_access": ["user_123", "user_456"],
        "user_write_access": ["user_123"],
        "external_user_read_access": ["customer_123", "customer_456"],
        "external_user_write_access": ["customer_123"]
      }
    }
  2. Workspace-Level Permissions

    {
      "metadata": {
        "workspace_read_access": ["workspace_789", "workspace_012"],
        "workspace_write_access": ["workspace_789"]
      }
    }
  3. Role-Level Permissions

    {
      "metadata": {
        "role_read_access": ["admin", "editor", "viewer"],
        "role_write_access": ["admin", "editor"]
      }
    }

Role-Based Access Control

While the API does not provide explicit endpoints for role management, you can implement role-based access control through the metadata fields. Common roles might include:

  • Admin: Full access to create, read, update and delete memories
  • Editor: Can read and update memories but not delete them
  • Viewer: Read-only access to memories

These roles are implemented by consistently using the same role identifiers in the role_read_access and role_write_access metadata fields.

Implementation

Authentication

The Papr API supports multiple authentication methods:

# Initialize client with API key
client = PaprMemory(api_key="your_api_key")

# Or initialize with bearer token
client = PaprMemory(bearer_token="your_bearer_token")

# Or initialize with session token
client = PaprMemory(session_token="your_session_token")

Implementing Access Control

# Create memory with access control
memory = client.memory.add(
    content="Quarterly financial report",
    type="text",
    metadata={
        "workspace_id": "finance_workspace",
        "topics": "finance, quarterly, confidential",
        "user_read_access": ["finance_team_lead", "cfo", "ceo"],
        "user_write_access": ["finance_team_lead"],
        "role_read_access": ["finance_department", "executive"],
        "role_write_access": ["finance_department_lead"]
    }
)

# Filter memories by access control in search
results = client.memory.search(
    query="Find quarterly financial reports",
    metadata={
        "user_id": "finance_team_lead",
        "workspace_id": "finance_workspace"
    }
)

Security Features

Data Isolation

  • Isolation through metadata-based access control
  • Separate metadata filtering for searches
  • Memory-level access restrictions

Access Controls

  • Fine-grained permissions through metadata
  • User, workspace, and role-based access
  • Extensible metadata attributes

Audit Logging

  • Access logs through memory interactions
  • Search and retrieval tracking
  • Content modification history

Compliance

  • GDPR compliance through metadata filtering
  • Data retention controls
  • Access control for regulatory requirements

Data Segregation

Papr Memory implements data segregation with multiple security layers to ensure complete isolation between developers, organizations, and end users. This section provides technical details about how we guarantee your data remains secure and private. To segregate data at the database level contact us to enable full segregation.

Multi-Layer Security Architecture

Our system employs a defense-in-depth approach with multiple independent security layers:

1. Authentication-Level Isolation

API Key Scoping: Each API key is bound to a specific organization, creating the first layer of isolation:

# Each developer gets their own isolated environment
client = PaprMemory(api_key="apiKey")

# This API key can ONLY access:
# - Organization: abc123
# - No cross-organization access possible

2. Database-Level Access Control

Mandatory ACL Filtering: Every database query includes automatic access control filters:

# Example: All memory searches automatically include ACL filters
acl_filter = {
    "$or": [
        {"user_id": {"$eq": str(user_id)}},
        {"user_read_access": {"$in": [str(user_id)]}},
        {"workspace_read_access": {"$in": user_workspace_ids}},
        {"namespace_read_access": {"$in": user_namespace_ids}},
        {"role_read_access": {"$in": user_roles}},
        {"external_user_read_access": {"$in": [external_user_id]}}
    ]
}

# This filter is applied to EVERY query - no exceptions with additional verifications

Six-Layer ACL System: We implement comprehensive access control through metadata:

  • user_read_access / user_write_access: Individual user permissions
  • workspace_read_access / workspace_write_access: Team-level permissions
  • role_read_access / role_write_access: Role-based permissions
  • external_user_read_access / external_user_write_access: Developer's end-user permissions

3. Vector Database Isolation

Filtered Vector Search: Even AI similarity searches are user-scoped:

# Vector searches include the same ACL filters
query_result = await vector_index.query(
    vector=embedding,
    filter=acl_filter,  # Same ACL enforcement at vector level
    top_k=10
)

This ensures that semantic search and AI-powered features respect user boundaries.

4. Application-Level Enforcement

Default-Private Design: All memories are private by default:

# When creating a memory, default ACLs are automatically applied at an api/org/namespace level. Pass user and external_user_ids to segregate between your users.
memory = client.memory.add(
    content="User's private data",
    # Automatically gets:
    # metadata.user_read_access = [current_user_id]
    # metadata.user_write_access = [current_user_id]
)

Explicit Sharing Required: Access must be explicitly granted, never assumed:

# Sharing requires explicit permission grants
shared_memory = client.memory.add(
    content="Shared project data",
    metadata={
        "user_read_access": ["user1", "user2", "user3"],  # Explicit list
        "user_write_access": ["user1"]  # Explicit write permissions
    }
)

Technical Isolation Mechanisms

Organization-Level Isolation

Each developer operates within a completely isolated organization:

{
  "organization_id": "org_developer_abc123",
  "namespace_id": "production",
  "api_key_scope": {
    "organization": "org_developer_abc123",
    "namespace": "production",
    "permissions": ["memory:read", "memory:write", "user:manage"]
  }
}

Cross-Organization Access: Technically impossible - there is no API endpoint or database query that can access data across organization boundaries.

User-Level Isolation

Within an organization, users are strictly isolated:

# User A's memories
memory_a = client.memory.add(
    content="User A's private notes",
    metadata={
        "external_user_id": "user_a",
        "user_read_access": ["user_a"],
        "user_write_access": ["user_a"]
    }
)

# User B cannot access User A's memories
# This search will return 0 results for User B
results = client.memory.search(
    query="private notes",
    external_user_id="user_b"  # Only sees User B's accessible memories
)

Workspace and Role Isolation

Fine-grained control within organizations:

# Engineering workspace memories
eng_memory = client.memory.add(
    content="Engineering team discussion",
    metadata={
        "workspace_read_access": ["engineering"],
        "role_read_access": ["engineer", "tech_lead"]
    }
)

# Finance team members cannot access engineering memories
# Even if they try to search, ACL filters prevent access

Comprehensive Access Point Protection

Every data access point is protected:

Memory Creation: Automatic ACL assignment
Memory Search: Mandatory ACL filtering
Memory Retrieval: ID-based access control
Vector Similarity Search: Filtered embeddings
Graph Traversal: Relationship-aware ACLs
Batch Operations: Bulk ACL enforcement
Background Processing: Maintained user context

Data Leakage Prevention

No Technical Pathway for Cross-User Access:

  1. API Level: Authentication resolves to specific users before any data access
  2. Database Level: All queries include mandatory user filters
  3. Vector Level: Similarity searches are user-scoped
  4. Cache Level: Cached results maintain ACL boundaries
  5. Background Jobs: Async processing preserves user context

Example - What Happens When User B Tries to Access User A's Memory:

# User B attempts to access User A's memory by ID
try:
    memory = client.memory.get("user_a_memory_id", external_user_id="user_b")
    # This will fail because:
    # 1. Authentication resolves to User B's context
    # 2. Database query includes ACL filter for User B
    # 3. User A's memory is not in User B's accessible set
    # Result: Memory not found (404) - no data leaked
except MemoryNotFoundError:
    print("Memory not accessible - proper isolation working")

Encryption and Transport Security

Data Protection at Rest and in Transit:

  • AES-256 Encryption: All stored memories encrypted at rest
  • TLS 1.3: All API communications encrypted in transit
  • Client-Side Encryption: Optional additional encryption layer
  • Key Management: Secure key rotation and management

Audit and Compliance

Complete Audit Trail:

# All access is logged with full context
audit_log = {
    "user_id": "user_123",
    "organization_id": "org_abc123", 
    "action": "memory.search",
    "query": "project notes",
    "results_count": 5,
    "timestamp": "2024-01-15T10:30:00Z",
    "acl_filters_applied": ["user_read_access", "workspace_read_access"]
}

Compliance Features:

  • GDPR compliance through metadata filtering
  • Data retention controls per organization
  • Access pattern monitoring
  • Regulatory audit support

Developer Confidence Statement

You can confidently tell your developers:

"Papr Memory has multiple independent isolation layers. Each end user's data is completely segregated through authentication scoping, database-level ACLs, and application-level access controls. There is no technical pathway for one user to access another user's memories - every database query, vector search, and API endpoint enforces strict user isolation. The system is designed with a 'secure by default' approach where access must be explicitly granted rather than restricted."

Security Guarantees

  1. Authentication Isolation: API keys are organization and namespace scoped
  2. Database Isolation: All queries include mandatory user filters
  3. Vector Search Isolation: AI similarity searches are user-scoped
  4. Multi-Tenant Isolation: Organization boundaries prevent cross-tenant access
  5. Default Privacy: New memories are private unless explicitly shared
  6. Explicit Sharing: Access must be explicitly granted through ACL metadata
  7. Comprehensive Coverage: Every access point enforces isolation
  8. Audit Compliance: Complete access logging and monitoring
  9. Database/namespace level segregation: Enterprise customers can get database and namespace level segregation. Contact us to enable this

Performance Impact

Security with Performance: Our isolation mechanisms are designed for minimal performance impact:

  • ACL filters use optimized database indexes
  • Vector and knowledge graph searches include filters at the database level
  • Caching respects ACL boundaries
  • Bulk operations maintain efficiency while enforcing security

Best Practices

  1. Access Control Design

    • Establish consistent naming conventions for roles
    • Create logical workspace structures
    • Document access control schema
    • Regularly review access patterns
  2. User Management

    • Use descriptive external IDs
    • Add comprehensive user metadata
    • Maintain up-to-date user records
    • Plan for user lifecycle management
    • Consider automatic user creation via external_user_id for simpler workflows
  3. Security

    • Rotate API keys regularly
    • Apply principle of least privilege in access controls
    • Monitor access patterns
    • Implement rate limiting
  4. Compliance

    • Document data flows
    • Set retention policies
    • Implement comprehensive metadata
    • Regular compliance reviews