Permission & Security Model
Status: Draft
Overview
The permission model defines what applets can and cannot do. It operates at three levels:
graph TB
subgraph "Permission Levels"
L1[Installation Permissions<br/>What applet declares it needs]
L2[Tenant Permissions<br/>What tenants allow]
L3[User Permissions<br/>What users can do]
end
L1 --> L2
L2 --> L3
style L1 fill:#3b82f6,stroke:#1e40af,color:#fff
style L2 fill:#10b981,stroke:#047857,color:#fff
style L3 fill:#f59e0b,stroke:#d97706,color:#fff
Permission Categories
mindmap
root((Permissions))
Database
Read tables
Write tables
Create tables
HTTP
External hosts
Blocked IPs
Events
Subscribe
Publish
UI
Navigation
Pages
Widgets
Secrets
API keys
Tokens
1. Database Permissions
permissions:
database:
read:
- clients
- chats
- chat_messages
write:
- clients
- chats
createTables: true
| Access Type | Capabilities |
|---|---|
| Read | SELECT queries only, automatic tenant_id filtering, row limit (1000), query timeout (5s) |
| Write | INSERT, UPDATE, DELETE, automatic tenant_id injection, audit logging |
| Create Tables | Requires admin approval, prefixed with applet_{id}_, automatic tenant_id column |
2. External HTTP Permissions
permissions:
http:
external:
- "api.openai.com"
- "*.dify.ai"
- "api.stripe.com"
Allowed:
- HTTPS connections to declared hosts
- Wildcard subdomains (*.example.com)
- Standard HTTP methods
Blocked (Always):
- Private IP ranges (10.x, 172.16.x, 192.168.x)
- Localhost (127.0.0.1, ::1)
- Cloud metadata endpoints (169.254.169.254)
- Non-HTTPS connections (configurable)
3. Event Permissions
permissions:
events:
subscribe:
- "chat.message.created"
- "client.created"
publish:
- "ai.response.generated"
4. UI Permissions
permissions:
ui:
navigation: true
pages: true
widgets: true
5. Secret Permissions
permissions:
secrets:
- name: "OPENAI_API_KEY"
required: true
- name: "WEBHOOK_SECRET"
required: false
Permission Enforcement
Installation Flow
flowchart TB
START[Upload Applet Package] --> PARSE[Parse Manifest]
PARSE --> EXTRACT[Extract Permissions]
EXTRACT --> DISPLAY[Display Permission Summary]
DISPLAY --> REVIEW{Admin Review}
REVIEW -->|Approve| INSTALL[Install with Approved Permissions]
REVIEW -->|Reject| CANCEL[Cancel Installation]
REVIEW -->|Review Tables| TABLES[Review Table Definitions]
TABLES --> REVIEW
style START fill:#3b82f6,stroke:#1e40af,color:#fff
style INSTALL fill:#10b981,stroke:#047857,color:#fff
style CANCEL fill:#ef4444,stroke:#b91c1c,color:#fff
Runtime Enforcement
type PermissionEnforcer struct {
allowedTables map[string]TablePermission
allowedHosts []string
allowedEvents []string
publishableEvents []string
}
func (e *PermissionEnforcer) CheckDatabaseAccess(table string, operation string) error {
perm, ok := e.allowedTables[table]
if !ok {
return ErrTableNotAllowed{Table: table}
}
if operation == "write" && !perm.Write {
return ErrWriteNotAllowed{Table: table}
}
return nil
}
func (e *PermissionEnforcer) CheckHTTPAccess(host string) error {
for _, allowed := range e.allowedHosts {
if matchHost(host, allowed) {
return nil
}
}
return ErrHostNotAllowed{Host: host}
}
Database Query Interception
sequenceDiagram
participant Applet
participant Proxy as Database Proxy
participant Enforcer as Permission Enforcer
participant DB as Database
Applet->>Proxy: Query(sql, args)
Proxy->>Enforcer: CheckDatabaseAccess(tables)
Enforcer-->>Proxy: OK / Error
alt Permission Denied
Proxy-->>Applet: Error: Table not allowed
else Permission OK
Proxy->>Proxy: Inject tenant_id filter
Proxy->>Proxy: Add row limit
Proxy->>DB: Execute with timeout
DB-->>Proxy: Results
Proxy-->>Applet: Filtered results
end
User-Level Permissions
Applet Permission Keys
Applets define their own permission keys:
appletPermissions:
- key: "ai-chat.config.read"
name: { en: "View AI Chat Configuration" }
- key: "ai-chat.config.write"
name: { en: "Edit AI Chat Configuration" }
- key: "ai-chat.assist"
name: { en: "Use AI Assistant" }
Integration with SDK RBAC
// Applet permissions are registered with SDK
func (a *Applet) RegisterPermissions(app application.Application) {
for _, perm := range a.Manifest.AppletPermissions {
app.RegisterPermission(permission.Permission{
Key: fmt.Sprintf("applet.%s.%s", a.ID, perm.Key),
Name: perm.Name,
Description: perm.Description,
Module: fmt.Sprintf("applet-%s", a.ID),
})
}
}
// Checked in handlers
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !sdkcomposables.CanUser(r.Context(), "applet.ai-chat.config.write") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// ... handle request
}
Security Boundaries
Sandboxing Layers
graph TB
subgraph "Security Layers"
L5[Layer 5: User Permissions - RBAC]
L4[Layer 4: Tenant Isolation]
L3[Layer 3: Permission Enforcement]
L2[Layer 2: Runtime Sandboxing]
L1[Layer 1: Input Validation]
end
L5 --> L4
L4 --> L3
L3 --> L2
L2 --> L1
style L5 fill:#8b5cf6,stroke:#5b21b6,color:#fff
style L4 fill:#3b82f6,stroke:#1e40af,color:#fff
style L3 fill:#10b981,stroke:#047857,color:#fff
style L2 fill:#f59e0b,stroke:#d97706,color:#fff
style L1 fill:#ef4444,stroke:#b91c1c,color:#fff
| Layer | Purpose |
|---|---|
| Layer 5 | What can this user do within the applet? |
| Layer 4 | All data queries filtered by tenant_id |
| Layer 3 | Applet can only access declared resources |
| Layer 2 | Process isolation, resource limits |
| Layer 1 | All inputs sanitized, SQL parameterized |
Threat Model
| Threat | Mitigation |
|---|---|
| SQL Injection | Parameterized queries only, no raw SQL |
| Cross-Tenant Access | Automatic tenant_id filtering |
| SSRF | Host allowlist, IP validation |
| Resource Exhaustion | CPU/memory/time limits |
| Data Exfiltration | Audit logging, rate limits |
| Privilege Escalation | Permission checks at every layer |
| Code Injection | No eval(), no dynamic code execution |
Audit Logging
All applet actions are logged:
type AuditLog struct {
Timestamp time.Time
AppletID string
TenantID string
UserID *uint
Action string // "db.query", "http.request", "event.publish"
Resource string // Table name, URL, event type
Details JSONB // Query, request body, etc.
Success bool
Error *string
DurationMs int
}
Rate Limiting
# Per-applet limits
rateLimits:
http:
requestsPerMinute: 1000
requestsPerHour: 10000
database:
queriesPerMinute: 100
rowsPerMinute: 10000
events:
publishPerMinute: 30
external:
requestsPerMinute: 60
Emergency Controls
flowchart LR
ERROR[Errors Detected] --> THRESHOLD{Threshold Exceeded?}
THRESHOLD -->|Yes| TRIP[Trip Circuit Breaker]
THRESHOLD -->|No| CONTINUE[Continue]
TRIP --> COOLDOWN[Cooldown Period]
COOLDOWN --> RETRY[Retry]
RETRY --> THRESHOLD
ADMIN[Admin Action] --> DISABLE[Emergency Disable]
DISABLE --> STOP[Stop All Handlers]
STOP --> LOG[Log Incident]
LOG --> NOTIFY[Notify Admins]
style TRIP fill:#ef4444,stroke:#b91c1c,color:#fff
style DISABLE fill:#ef4444,stroke:#b91c1c,color:#fff
// Circuit breaker for misbehaving applets
type CircuitBreaker struct {
ErrorThreshold int // Errors before tripping
ErrorWindow time.Duration // Window to count errors
CooldownDuration time.Duration // How long to stay open
}
// Admin can disable applet instantly
func (m *Manager) EmergencyDisable(appletID string, reason string) error {
// Stop all handlers
// Log incident
// Notify admins
// Preserve state for investigation
}
Next Steps
- Review Database for data access patterns
- See Distribution for packaging
- Check Architecture for system design