diff --git a/tutorials/01_complete_basic_syntax.ksl b/tutorials/01_complete_basic_syntax.ksl new file mode 100644 index 0000000..81b154f --- /dev/null +++ b/tutorials/01_complete_basic_syntax.ksl @@ -0,0 +1,55 @@ +version 0.1 +namespace learning + +// Basic type with no relations +public type user { + +} + +// Type demonstrating ALL cardinality constraints +public type document { + // ExactlyOne - must have exactly one + relation owner: [ExactlyOne user] + + // Any - can have zero, one, or unlimited + relation editor: [Any user] + + // AtMostOne - can have zero or one + relation reviewer: [AtMostOne user] + + // AtLeastOne - must have one or more + relation approver: [AtLeastOne user] + + // Basic permission - direct reference + relation can_read: owner + + // Union (OR) - multiple ways to get permission + relation can_edit: owner or editor + + // Complex union - three ways to get permission + relation can_view: owner or editor or reviewer + + // Intersection (AND) - must satisfy both conditions + relation can_approve: approver and owner + + // Exclusion (UNLESS) - first condition unless second is true + relation can_delete: owner unless reviewer + + // Grouping with parentheses for complex logic + relation can_publish: (owner or editor) and approver +} + +// Demonstrating ALL visibility modifiers +public type project { + // Public - accessible from other namespaces + public relation owner: [ExactlyOne user] + + // Internal - accessible within this namespace only + internal relation team_member: [Any user] + + // Private - accessible within this type only + private relation secret_info: [Any user] + + // Default visibility (public if not specified) + relation contributor: [Any user] +} diff --git a/tutorials/01_complete_basic_syntax.zed b/tutorials/01_complete_basic_syntax.zed new file mode 100644 index 0000000..271a296 --- /dev/null +++ b/tutorials/01_complete_basic_syntax.zed @@ -0,0 +1,29 @@ +definition learning/document { + permission approver = t_approver + relation t_approver: learning/user + permission can_approve = (approver & owner) + permission can_delete = (owner - reviewer) + permission can_edit = owner + editor + permission can_publish = (owner + editor & approver) + permission can_read = owner + permission can_view = owner + editor + reviewer + permission editor = t_editor + relation t_editor: learning/user + permission owner = t_owner + relation t_owner: learning/user + permission reviewer = t_reviewer + relation t_reviewer: learning/user +} + +definition learning/project { + permission contributor = t_contributor + relation t_contributor: learning/user + permission owner = t_owner + relation t_owner: learning/user + permission secret_info = t_secret_info + relation t_secret_info: learning/user + permission team_member = t_team_member + relation t_team_member: learning/user +} + +definition learning/user {} \ No newline at end of file diff --git a/tutorials/02_advanced_and_cross_namespace.zed b/tutorials/02_advanced_and_cross_namespace.zed new file mode 100644 index 0000000..8bc2fbf --- /dev/null +++ b/tutorials/02_advanced_and_cross_namespace.zed @@ -0,0 +1,41 @@ +definition files/document { + permission author = t_author + relation t_author: organization/user + permission can_read = author + t_folder->can_read + permission can_write = author + t_folder->can_manage + permission folder = t_folder + relation t_folder: files/folder + permission shared_with = t_shared_with + relation t_shared_with: organization/team#member +} + +definition files/folder { + permission can_manage = owner + t_project->can_manage + permission can_read = owner + t_project->team_members + t_parent->can_read + permission owner = t_owner + relation t_owner: organization/user + permission parent = t_parent + relation t_parent: files/folder + permission project = t_project + relation t_project: organization/project +} + +definition organization/project { + permission can_manage = t_owner_team->can_manage + t_owner_team->member + permission collaborator_teams = t_collaborator_teams + relation t_collaborator_teams: organization/team + permission managers = t_owner_team->manager + permission owner_team = t_owner_team + relation t_owner_team: organization/team + permission team_members = t_owner_team->member + t_collaborator_teams->member +} + +definition organization/team { + permission can_manage = manager + permission manager = t_manager + relation t_manager: organization/user + permission member = t_member + relation t_member: organization/user +} + +definition organization/user {} \ No newline at end of file diff --git a/tutorials/02_advanced_relations.ksl b/tutorials/02_advanced_relations.ksl new file mode 100644 index 0000000..c5a0682 --- /dev/null +++ b/tutorials/02_advanced_relations.ksl @@ -0,0 +1,28 @@ +version 0.1 +namespace organization + +public type user { + +} + +public type team { + relation member: [Any user] + relation manager: [ExactlyOne user] + + // Permission derived from relations + relation can_manage: manager +} + +public type project { + relation owner_team: [ExactlyOne team] + relation collaborator_teams: [Any team] + + // Nested relation access - access team's members through the relation + relation team_members: owner_team.member or collaborator_teams.member + + // Nested relation with specific sub-relation + relation managers: owner_team.manager + + // Complex nested permissions + relation can_manage: owner_team.can_manage or owner_team.member +} diff --git a/tutorials/02_cross_namespace.ksl b/tutorials/02_cross_namespace.ksl new file mode 100644 index 0000000..0e30406 --- /dev/null +++ b/tutorials/02_cross_namespace.ksl @@ -0,0 +1,28 @@ +version 0.1 +namespace files +import organization + +public type folder { + // Cross-namespace type reference + relation owner: [ExactlyOne organization.user] + relation parent: [AtMostOne folder] + relation project: [AtMostOne organization.project] + + // Cross-namespace nested relations + relation can_read: owner or project.team_members or parent.can_read + + // Complex cross-namespace permissions + relation can_manage: owner or project.can_manage +} + +public type document { + relation folder: [ExactlyOne folder] + relation author: [ExactlyOne organization.user] + + // Inherit permissions from folder + relation can_read: author or folder.can_read + relation can_write: author or folder.can_manage + + // Self-referencing with cross-namespace + relation shared_with: [Any organization.team.member] +} diff --git a/tutorials/03_complete_extensions.zed b/tutorials/03_complete_extensions.zed new file mode 100644 index 0000000..4f37890 --- /dev/null +++ b/tutorials/03_complete_extensions.zed @@ -0,0 +1,50 @@ +definition inventory/server { + permission can_delete = t_workspace->servers_delete + permission can_read = t_workspace->servers_read + permission can_write = t_workspace->servers_write + permission workspace = t_workspace + relation t_workspace: rbac/workspace +} + +definition rbac/principal {} + +definition rbac/role { + permission global_all_permissions = t_global_all_permissions + relation t_global_all_permissions: rbac/principal:* + permission mod_inventory_all_rel_can_delete = t_mod_inventory_all_rel_can_delete + relation t_mod_inventory_all_rel_can_delete: rbac/principal:* + permission mod_inventory_all_rel_can_read = t_mod_inventory_all_rel_can_read + relation t_mod_inventory_all_rel_can_read: rbac/principal:* + permission mod_inventory_all_rel_can_write = t_mod_inventory_all_rel_can_write + relation t_mod_inventory_all_rel_can_write: rbac/principal:* + permission mod_inventory_type_server_all = t_mod_inventory_type_server_all + relation t_mod_inventory_type_server_all: rbac/principal:* + permission servers_delete = t_servers_delete + relation t_servers_delete: rbac/principal:* + permission servers_read = t_servers_read + relation t_servers_read: rbac/principal:* + permission servers_write = t_servers_write + relation t_servers_write: rbac/principal:* +} + +definition rbac/role_binding { + permission granted = t_granted + relation t_granted: rbac/role + permission servers_delete = (subject & t_granted->servers_delete + t_granted->global_all_permissions + t_granted->mod_inventory_type_server_all + t_granted->mod_inventory_all_rel_can_delete) + permission servers_read = (subject & t_granted->servers_read + t_granted->global_all_permissions + t_granted->mod_inventory_type_server_all + t_granted->mod_inventory_all_rel_can_read) + permission servers_write = (subject & t_granted->servers_write + t_granted->global_all_permissions + t_granted->mod_inventory_type_server_all + t_granted->mod_inventory_all_rel_can_write) + permission subject = t_subject + relation t_subject: rbac/user +} + +definition rbac/user {} + +definition rbac/workspace { + permission parent = t_parent + relation t_parent: rbac/workspace + permission servers_delete = t_user_grant->servers_delete + t_parent->servers_delete + permission servers_read = t_user_grant->servers_read + t_parent->servers_read + permission servers_write = t_user_grant->servers_write + t_parent->servers_write + permission user_grant = t_user_grant + relation t_user_grant: rbac/role_binding +} \ No newline at end of file diff --git a/tutorials/03_extension_usage.ksl b/tutorials/03_extension_usage.ksl new file mode 100644 index 0000000..089c4ea --- /dev/null +++ b/tutorials/03_extension_usage.ksl @@ -0,0 +1,17 @@ +version 0.1 +namespace inventory +import rbac + +public type server { + private relation workspace: [ExactlyOne rbac.workspace] + + // Using the extension with @ syntax + @rbac.workspace_permission(permission_name:'servers_read') + public relation can_read: workspace.servers_read + + @rbac.workspace_permission(permission_name:'servers_write') + public relation can_write: workspace.servers_write + + @rbac.workspace_permission(permission_name:'servers_delete') + public relation can_delete: workspace.servers_delete +} diff --git a/tutorials/04_complete_real_world.ksl b/tutorials/04_complete_real_world.ksl new file mode 100644 index 0000000..72987e0 --- /dev/null +++ b/tutorials/04_complete_real_world.ksl @@ -0,0 +1,56 @@ +version 0.1 +namespace enterprise +import rbac + +// Complete real-world example using EVERY KSL feature +public type application { + // Cross-namespace import + private relation workspace: [ExactlyOne rbac.workspace] + + // All cardinality types + relation owner: [ExactlyOne rbac.user] // ExactlyOne + relation developers: [Any rbac.user] // Any + relation tech_lead: [AtMostOne rbac.user] // AtMostOne + relation stakeholders: [AtLeastOne rbac.user] // AtLeastOne + + // All visibility types + public relation can_deploy: owner or tech_lead + internal relation can_debug: developers or tech_lead + private relation admin_access: owner + + // Complex relation expressions + relation can_read: owner or developers or stakeholders + relation can_write: (owner or tech_lead) and developers + relation can_manage: owner unless tech_lead + relation emergency_access: (owner or tech_lead) and stakeholders + + // Cross-namespace nested relations + relation workspace_access: workspace.user_grant + + // Extensions usage + @rbac.workspace_permission(permission_name:'app_deploy') + public relation deploy_permission: workspace.app_deploy + + @rbac.workspace_permission(permission_name:'app_monitor') + public relation monitor_permission: workspace.app_monitor +} + +// Demonstrating complex hierarchies +public type environment { + relation application: [ExactlyOne application] + relation environment_manager: [ExactlyOne rbac.user] + + // Inherit from application with additional restrictions + relation can_deploy: environment_manager and application.can_deploy + relation can_read: environment_manager or application.can_read + + // Self-referencing hierarchy + relation parent_env: [AtMostOne environment] + relation inherited_access: parent_env.can_read or parent_env.can_deploy +} + +// Using reserved keyword with escape +public type database { + relation #version: [ExactlyOne rbac.user] // Using # to escape reserved word + relation backup_access: [Any rbac.user] +} \ No newline at end of file diff --git a/tutorials/04_complete_real_world.zed b/tutorials/04_complete_real_world.zed new file mode 100644 index 0000000..a17e80c --- /dev/null +++ b/tutorials/04_complete_real_world.zed @@ -0,0 +1,78 @@ +definition enterprise/application { + permission admin_access = owner + permission can_debug = developers + tech_lead + permission can_deploy = owner + tech_lead + permission can_manage = (owner - tech_lead) + permission can_read = owner + developers + stakeholders + permission can_write = (owner + tech_lead & developers) + permission deploy_permission = t_workspace->app_deploy + permission developers = t_developers + relation t_developers: rbac/user + permission emergency_access = (owner + tech_lead & stakeholders) + permission monitor_permission = t_workspace->app_monitor + permission owner = t_owner + relation t_owner: rbac/user + permission stakeholders = t_stakeholders + relation t_stakeholders: rbac/user + permission tech_lead = t_tech_lead + relation t_tech_lead: rbac/user + permission workspace = t_workspace + relation t_workspace: rbac/workspace + permission workspace_access = t_workspace->user_grant +} + +definition enterprise/database { + permission backup_access = t_backup_access + relation t_backup_access: rbac/user + permission version = t_version + relation t_version: rbac/user +} + +definition enterprise/environment { + permission application = t_application + relation t_application: enterprise/application + permission can_deploy = (environment_manager & t_application->can_deploy) + permission can_read = environment_manager + t_application->can_read + permission environment_manager = t_environment_manager + relation t_environment_manager: rbac/user + permission inherited_access = t_parent_env->can_read + t_parent_env->can_deploy + permission parent_env = t_parent_env + relation t_parent_env: enterprise/environment +} + +definition rbac/principal {} + +definition rbac/role { + permission app_deploy = t_app_deploy + relation t_app_deploy: rbac/principal:* + permission app_monitor = t_app_monitor + relation t_app_monitor: rbac/principal:* + permission global_all_permissions = t_global_all_permissions + relation t_global_all_permissions: rbac/principal:* + permission mod_enterprise_all_rel_deploy_permission = t_mod_enterprise_all_rel_deploy_permission + relation t_mod_enterprise_all_rel_deploy_permission: rbac/principal:* + permission mod_enterprise_all_rel_monitor_permission = t_mod_enterprise_all_rel_monitor_permission + relation t_mod_enterprise_all_rel_monitor_permission: rbac/principal:* + permission mod_enterprise_type_application_all = t_mod_enterprise_type_application_all + relation t_mod_enterprise_type_application_all: rbac/principal:* +} + +definition rbac/role_binding { + permission app_deploy = (subject & t_granted->app_deploy + t_granted->global_all_permissions + t_granted->mod_enterprise_type_application_all + t_granted->mod_enterprise_all_rel_deploy_permission) + permission app_monitor = (subject & t_granted->app_monitor + t_granted->global_all_permissions + t_granted->mod_enterprise_type_application_all + t_granted->mod_enterprise_all_rel_monitor_permission) + permission granted = t_granted + relation t_granted: rbac/role + permission subject = t_subject + relation t_subject: rbac/user +} + +definition rbac/user {} + +definition rbac/workspace { + permission app_deploy = t_user_grant->app_deploy + t_parent->app_deploy + permission app_monitor = t_user_grant->app_monitor + t_parent->app_monitor + permission parent = t_parent + relation t_parent: rbac/workspace + permission user_grant = t_user_grant + relation t_user_grant: rbac/role_binding +} \ No newline at end of file diff --git a/tutorials/README.md b/tutorials/README.md new file mode 100644 index 0000000..2179e6e --- /dev/null +++ b/tutorials/README.md @@ -0,0 +1,70 @@ +# Minimal KSL Learning Guide + +Learn **ALL KSL syntax** with just 4 files! Each file teaches unique concepts with zero redundancy. + +## 📚 Complete Learning Path (30 minutes total) + +### **File 1: All Basic Syntax** (10 min) +- `01_complete_basic_syntax.ksl` + `.zed` +- **Teaches**: Types, all cardinalities, all visibility modifiers, all relation expressions + +### **File 2: Advanced Relations & Cross-Namespace** (10 min) +- `02_advanced_relations.ksl` + `02_cross_namespace.ksl` → `02_advanced_and_cross_namespace.zed` +- **Teaches**: Nested relations, imports, cross-namespace references + +### **File 3: Extension System** (10 min) +- `rbac_foundation.ksl` + `03_extension_usage.ksl` → `03_complete_extensions.zed` +- **Teaches**: Templates, dynamic generation, `${variables}`, extension usage + +### **File 4: Real-World Complete Example** (bonus) +- `rbac_foundation.ksl` + `04_complete_real_world.ksl` → `04_complete_real_world.zed` +- **Teaches**: Everything combined in a practical enterprise scenario + +## 🎯 What Each File Covers + +| Syntax Feature | File 1 | File 2 | File 3 | File 4 | +|----------------|--------|--------|--------|--------| +| **Basic Types** | ✅ | | | ✅ | +| **All Cardinality** (ExactlyOne, Any, AtMostOne, AtLeastOne) | ✅ | | | ✅ | +| **Visibility** (public, internal, private) | ✅ | | | ✅ | +| **Relations** (direct, union, intersection, exclusion) | ✅ | | | ✅ | +| **Nested Relations** (team.member) | | ✅ | | ✅ | +| **Cross-Namespace** (import, namespace.type) | | ✅ | | ✅ | +| **Extensions** (templates, ${variables}) | | | ✅ | ✅ | +| **Reserved Keywords** (#version) | | | | ✅ | + +## 🚀 Quick Compilation + +```bash +# Tutorial 1 (single file) +../bin/ksl -o 01_complete_basic_syntax.zed 01_complete_basic_syntax.ksl + +# Tutorial 2 (multiple source files) +../bin/ksl -o 02_advanced_and_cross_namespace.zed 02_advanced_relations.ksl 02_cross_namespace.ksl + +# Tutorial 3 (extension system) +../bin/ksl -o 03_complete_extensions.zed rbac_foundation.ksl 03_extension_usage.ksl + +# Tutorial 4 (real-world example using shared RBAC foundation) +../bin/ksl -o 04_complete_real_world.zed rbac_foundation.ksl 04_complete_real_world.ksl + +# All examples already compiled - just read the .zed files! +``` + +## 📖 Documentation Reference + +**Essential Reading:** +- `ksl_compiler_behavior.md` - **CRITICAL**: Special behaviors, magic mappings, gotchas + +**Cardinality Deep-Dive:** +- `cardinality_explained.md` - When to use each cardinality type + +## ✅ After Reading These 4 Files You'll Know: + +- Every KSL syntax feature +- When to use each cardinality constraint +- How to organize multi-namespace schemas +- How to create reusable authorization patterns with extensions +- Real-world enterprise authorization modeling + +**Total learning time: ~30 minutes to master the entire language!** diff --git a/tutorials/cardinality_explained.md b/tutorials/cardinality_explained.md new file mode 100644 index 0000000..c6d4b2d --- /dev/null +++ b/tutorials/cardinality_explained.md @@ -0,0 +1,92 @@ +# Cardinality Constraints Explained with Real Examples + +## The Key Point: These are BUSINESS RULES for your application + +Think of cardinality as **how many relationships are allowed**: + +## 1. ExactlyOne - Must have exactly 1 + +**Example: Document Owner** +``` +✅ VALID: document:123 owner user:alice +❌ INVALID: document:123 has no owner (missing required owner) +❌ INVALID: document:123 owner user:alice, user:bob (too many owners) +``` + +**Real-world meaning**: Every document must have exactly one owner, no more, no less. + +## 2. Any - Can have 0, 1, 2, 3, ... (unlimited) + +**Example: Document Editor** +``` +✅ VALID: document:123 has no editors (document can exist without editors) +✅ VALID: document:123 editor user:alice +✅ VALID: document:123 editor user:alice, user:bob, user:charlie +``` + +**Real-world meaning**: Documents can have any number of editors, including zero. + +## 3. AtMostOne - Can have 0 or 1 (but not more) + +**Example: Document Reviewer** +``` +✅ VALID: document:123 has no reviewer (review is optional) +✅ VALID: document:123 reviewer user:diana +❌ INVALID: document:123 reviewer user:diana, user:eve (can't have 2 reviewers) +``` + +**Real-world meaning**: Documents can optionally have one reviewer, but never multiple reviewers. + +## 4. AtLeastOne - Must have 1 or more + +**Example: Document Approver** +``` +❌ INVALID: document:123 has no approvers (approval is required) +✅ VALID: document:123 approver user:frank +✅ VALID: document:123 approver user:frank, user:george, user:helen +``` + +**Real-world meaning**: Documents must have at least one approver to be valid. + +## Summary Table + +| Cardinality | Min | Max | Examples | +|-------------|-----|-----|----------| +| `ExactlyOne` | 1 | 1 | Owner, Primary Contact, CEO | +| `Any` | 0 | ∞ | Editors, Viewers, Tags | +| `AtMostOne` | 0 | 1 | Reviewer, Assigned To, Current Status | +| `AtLeastOne` | 1 | ∞ | Approvers, Required Skills, Team Members | + +## In Your Application Code + +```python +def assign_owner(doc_id, user_id): + # ExactlyOne: Remove old owner, add new one + old_owners = get_relationships(doc_id, "owner") + for owner in old_owners: + delete_relationship(doc_id, "owner", owner) + add_relationship(doc_id, "owner", user_id) + +def add_editor(doc_id, user_id): + # Any: Just add, no validation needed + add_relationship(doc_id, "editor", user_id) + +def set_reviewer(doc_id, user_id): + # AtMostOne: Remove existing reviewer first + old_reviewers = get_relationships(doc_id, "reviewer") + for reviewer in old_reviewers: + delete_relationship(doc_id, "reviewer", reviewer) + if user_id: # Can be None (no reviewer) + add_relationship(doc_id, "reviewer", user_id) + +def add_approver(doc_id, user_id): + # AtLeastOne: Add approver, but validate at least one exists + add_relationship(doc_id, "approver", user_id) + # When removing, ensure at least one remains: + # if len(get_relationships(doc_id, "approver")) < 2: + # raise Error("Cannot remove last approver") +``` + +## Why You Don't See Difference in SpiceDB Schema + +SpiceDB doesn't enforce these constraints - it just stores relationships. Your application must implement the business logic to respect these cardinality rules. diff --git a/tutorials/ksl_compiler_behavior.md b/tutorials/ksl_compiler_behavior.md new file mode 100644 index 0000000..2801d9a --- /dev/null +++ b/tutorials/ksl_compiler_behavior.md @@ -0,0 +1,259 @@ +# KSL Compiler Behavior & Special Features + +Important behaviors and magic that happens during KSL compilation. + +## 🔮 Special Type Mappings + +### `[bool]` → `rbac/principal:*` + +**What you write:** +```ksl +relation permission: [bool] +``` + +**What gets generated:** +```zed +relation t_permission: rbac/principal:* +``` + +**Why:** The compiler hard-codes `[bool]` to mean "any principal" (universal identity type). + +**Requirements:** +- You **must** have a `principal` type in the `rbac` namespace +- This is not configurable (yet) + +**Source:** `/pkg/ksl/compiler.go` lines 15-19 + +## 🏷️ Naming Conventions & Transformations + +### Relation Name Prefixes + +**What you write:** +```ksl +relation owner: [ExactlyOne user] +``` + +**What gets generated:** +```zed +permission owner = t_owner +relation t_owner: namespace/user +``` + +**Rule:** All internal relations get `t_` prefix to avoid naming conflicts. + +### SpiceDB Naming Rules + +Generated names must match: `^[a-z][a-z0-9_]{1,62}[a-z0-9]$` + +**✅ Valid:** `owner`, `can_read`, `t_owner`, `global_all_permissions` +**❌ Invalid:** `_all_all`, `Owner`, `can-read`, `123permission` + +## 🔄 Visibility Behavior + +### Default Visibility + +**What you write:** +```ksl +type document { + relation owner: [ExactlyOne user] # No visibility specified +} +``` + +**What it becomes:** `public` (default visibility) + +### Internal Relations Still Generate Permissions + +Even `private` and `internal` relations generate SpiceDB permissions - visibility is a KSL concept, not enforced by SpiceDB. + +## 🎭 Extension System Magic + +### Built-in Template Variables + +When extensions are applied, the compiler automatically injects these variables: + +- `${NAMESPACE}` - The namespace where the extension is being applied +- `${TYPE}` - The type where the extension is being applied +- `${RELATION}` - The relation where the extension is being applied +- `${permission_name}` - Your custom parameter (example) + +**⚠️ Important:** `${MODULE}` was removed in a language refactor and no longer works. Use `${NAMESPACE}` instead. + +### Template Expansion Example + +**Extension definition:** +```ksl +public extension workspace_permission(permission_name) { + type role { + relation ${permission_name}: [bool] + relation `mod_${NAMESPACE}_type_${TYPE}_all`: [bool] + relation `mod_${NAMESPACE}_all_rel_${RELATION}`: [bool] + } +} +``` + +**When applied to:** +```ksl +namespace inventory +type server { + @rbac.workspace_permission(permission_name:'servers_read') + relation can_read: workspace.servers_read +} +``` + +**Variables get set to:** +- `${NAMESPACE}` = `"inventory"` +- `${TYPE}` = `"server"` +- `${RELATION}` = `"can_read"` +- `${permission_name}` = `"servers_read"` + +**Generated relations:** +```ksl +relation servers_read: [bool] # ${permission_name} +relation mod_inventory_type_server_all: [bool] # mod_${NAMESPACE}_type_${TYPE}_all +relation mod_inventory_all_rel_can_read: [bool] # mod_${NAMESPACE}_all_rel_${RELATION} +``` + +### `allow_duplicates` Keyword + +**What it does:** Prevents errors when the same relation is generated multiple times. + +**Example Problem:** +```ksl +type role { + // Without allow_duplicates, this fails if applied twice to same type + relation global_all_permissions: [bool] +} +``` + +**Solution:** +```ksl +type role { + // With allow_duplicates, second application is silently ignored + allow_duplicates relation global_all_permissions: [bool] +} +``` + +**When you need it:** +- Extensions that add the same "global" permissions to multiple resources +- Template relations that might be generated multiple times +- Shared permission patterns across different extension applications + +**Code behavior:** If a relation with the same name already exists, the duplicate is ignored instead of causing an error. + +### Variable Scoping & Availability + +**When variables are set:** +- Extension application triggers variable injection +- Each `@extension()` call gets its own variable context + +**Variable availability by context:** + +| Context | NAMESPACE | TYPE | RELATION | +|---------|-----------|------|----------| +| **Extension on namespace** | ✅ | ❌ | ❌ | +| **Extension on type** | ✅ | ✅ | ❌ | +| **Extension on relation** | ✅ | ✅ | ✅ | + +**Example showing context differences:** +```ksl +namespace inventory + +// Namespace-level extension: only NAMESPACE available +@rbac.namespace_permission() +type server { + // Type-level extension: NAMESPACE/TYPE available + @rbac.type_permission() + + // Relation-level extension: ALL variables available + @rbac.workspace_permission(permission_name:'servers_read') + relation can_read: workspace.servers_read +} +``` + +## ⚠️ Cardinality Constraints + +### Not Enforced by SpiceDB + +**Important:** Cardinality constraints are **documentation only**: + +```ksl +relation owner: [ExactlyOne user] # KSL constraint +``` + +SpiceDB will happily store multiple owners - your application must enforce the `ExactlyOne` rule. + +## 🔧 Reserved Keywords + +### Escaping with `#` + +**What you write:** +```ksl +relation #version: [ExactlyOne user] # 'version' is reserved +``` + +**What gets generated:** +```zed +relation version: namespace/user # # is stripped +``` + +## 🚨 Common Gotchas + +### 1. Missing `principal` Type +```ksl +# ❌ This will fail: +relation perm: [bool] # No principal type defined + +# ✅ This works: +public type principal { } +relation perm: [bool] +``` + +### 2. Invalid Template Names +```ksl +# ❌ Generates invalid names: +relation `_${NAMESPACE}_all`: [bool] # Starts with underscore + +# ✅ Valid naming: +relation `mod_${NAMESPACE}_all`: [bool] # Starts with letter +``` + +### 3. Cross-Namespace Without Import +```ksl +# ❌ This will fail: +relation owner: [ExactlyOne other_namespace.user] # No import + +# ✅ This works: +import other_namespace +relation owner: [ExactlyOne other_namespace.user] +``` + +### 4. Extension Parameter Quoting +```ksl +# ✅ Both work: +@rbac.permission(permission_name:'read_access') +@rbac.permission(permission_name:"read_access") + +# ❌ This fails: +@rbac.permission(permission_name:read_access) # Missing quotes +``` + +### 5. Using Outdated `${MODULE}` Variable +```ksl +# ❌ This fails (legacy variable removed): +relation `${MODULE}_all_permissions`: [bool] + +# ✅ Use current variable: +relation `${NAMESPACE}_all_permissions`: [bool] +``` + +**Note:** Many sample files in the repository still use `${MODULE}` but this is outdated and won't work. + +## 🎯 Best Practices + +1. **Always define `principal`** if using `[bool]` +2. **Use letter prefixes** in templates: `mod_${NAMESPACE}` not `${NAMESPACE}_` +3. **Import before cross-namespace** references +4. **Quote extension parameters** with single or double quotes +5. **Remember cardinality is documentation** - implement validation in your app + +This behavior is based on the current KSL compiler implementation and may change in future versions. diff --git a/tutorials/rbac_foundation.ksl b/tutorials/rbac_foundation.ksl new file mode 100644 index 0000000..23a556c --- /dev/null +++ b/tutorials/rbac_foundation.ksl @@ -0,0 +1,54 @@ +version 0.1 +namespace rbac + +// Required for [bool] type +public type principal { + +} + +public type user { + +} + +public type role { + +} + +public type role_binding { + relation subject: [ExactlyOne user] + relation granted: [AtLeastOne role] +} + +public type workspace { + relation parent: [AtMostOne workspace] + relation user_grant: [Any role_binding] +} + +// Extension with parameters - the most powerful KSL feature +public extension workspace_permission(permission_name) { + // Template variable: ${permission_name} gets replaced with parameter value + type role { + // Dynamic relation name using parameter + relation ${permission_name}: [bool] + + // Template with built-in variables: MODULE, TYPE, RELATION + allow_duplicates relation global_all_permissions: [bool] + allow_duplicates relation `mod_${NAMESPACE}_type_${TYPE}_all`: [bool] + allow_duplicates relation `mod_${NAMESPACE}_all_rel_${RELATION}`: [bool] + } + + // Complex templated logic + type role_binding { + relation ${permission_name}: subject and ( + granted.${permission_name} or + granted.global_all_permissions or + granted.`mod_${NAMESPACE}_type_${TYPE}_all` or + granted.`mod_${NAMESPACE}_all_rel_${RELATION}` + ) + } + + // Inheritance through workspace hierarchy + type workspace { + public relation ${permission_name}: user_grant.${permission_name} or parent.${permission_name} + } +}