# Security _Path: en/system/security_ ## Table of Contents - Security Model ## Content # Security Model Wippy implements attribute-based access control. Every request carries an actor (who) and a scope (what policies apply). Policies evaluate access based on the action, resource, and metadata from both actor and resource. ```mermaid flowchart LR A[Actor + Scope] --> PE[Policy Evaluation] --> AD[Allow/Deny] A -.->|Identity
Metadata| PE PE -.->|Conditions
actor, resource, action| AD ``` ## Entry Kinds | Kind | Description | |------|-------------| | `security.policy` | Declarative policy with conditions | | `security.policy.expr` | Expression-based policy | | `security.token_store` | Token storage and validation | ## Actors An actor represents who is performing an action. ```lua local security = require("security") -- Create actor with metadata local actor = security.new_actor("user:123", { role = "admin", team = "backend", department = "engineering", clearance = 3 }) -- Access actor properties local id = actor:id() -- "user:123" local meta = actor:meta() -- {role="admin", ...} ``` ### Actor in Context ```lua -- Get current actor from context local actor = security.actor() if not actor then return nil, errors.new("UNAUTHORIZED", "No actor in context") end ``` ## Policies Policies define access rules with actions, resources, conditions, and effects. ### Declarative Policy ```yaml # src/security/_index.yaml version: "1.0" namespace: app.security entries: # Admin full access - name: admin_policy kind: security.policy policy: actions: "*" resources: "*" effect: allow conditions: - field: actor.meta.role operator: eq value: admin groups: - admin # Read-only access - name: readonly_policy kind: security.policy policy: actions: - "*.read" - "*.get" - "*.list" resources: "*" effect: allow groups: - default # Resource owner access - name: owner_policy kind: security.policy policy: actions: - read - write - delete resources: "document:*" effect: allow conditions: - field: meta.owner operator: eq value_from: actor.id groups: - default # Deny confidential without clearance - name: deny_confidential kind: security.policy policy: actions: "*" resources: "document:*" effect: deny conditions: - field: meta.classification operator: eq value: confidential - field: actor.meta.clearance operator: lt value: 3 groups: - security ``` ### Policy Structure ```yaml policy: actions: "*" | "action" | ["action1", "action2"] resources: "*" | "resource" | ["res1", "res2"] effect: allow | deny conditions: # Optional - field: "field.path" operator: "eq" value: "static_value" # OR value_from: "other.field.path" ``` ### Expression-Based Policy For complex logic, use expression policies: ```yaml - name: flexible_access kind: security.policy.expr policy: actions: - read - write resources: "file:*" effect: allow expression: | (actor.meta.role == "editor" && action == "write") || (action == "read" && meta.public == true) || actor.id == meta.owner groups: - editors ``` ## Conditions Conditions allow dynamic policy evaluation based on actor, action, resource, and metadata. ### Field Paths | Path | Description | |------|-------------| | `actor.id` | Actor's unique identifier | | `actor.meta.*` | Actor metadata (supports nesting) | | `action` | The action being performed | | `resource` | The resource identifier | | `meta.*` | Resource metadata | ### Operators | Operator | Description | Example | |----------|-------------|---------| | `eq` | Equals | `actor.meta.role eq "admin"` | | `ne` | Not equals | `meta.status ne "deleted"` | | `lt` | Less than | `meta.priority lt 5` | | `gt` | Greater than | `actor.meta.clearance gt 2` | | `lte` | Less than or equal | `meta.size lte 1000` | | `gte` | Greater than or equal | `actor.meta.level gte 3` | | `in` | Value in array | `action in ["read", "write"]` | | `nin` | Value not in array | `meta.status nin ["deleted", "archived"]` | | `exists` | Field exists | `meta.owner exists true` | | `nexists` | Field not exists | `meta.deleted nexists true` | | `contains` | String contains | `resource contains "sensitive"` | | `ncontains` | String not contains | `resource ncontains "public"` | | `matches` | Regex match | `resource matches "^doc:.*"` | | `nmatches` | Regex not match | `actor.id nmatches "^system:.*"` | ### Condition Examples ```yaml # Match actor role conditions: - field: actor.meta.role operator: eq value: admin # Compare fields conditions: - field: meta.owner operator: eq value_from: actor.id # Numeric comparison conditions: - field: actor.meta.clearance operator: gte value: 3 # Array membership conditions: - field: actor.meta.role operator: in value: - admin - moderator # Pattern matching conditions: - field: resource operator: matches value: "^api:/v[0-9]+/admin/.*" # Multiple conditions (AND) conditions: - field: actor.meta.department operator: eq value: engineering - field: meta.environment operator: eq value: production ``` ## Scopes Scopes combine multiple policies into a security context. ```lua local security = require("security") -- Get policies local admin_policy = security.policy("app.security:admin_policy") local readonly_policy = security.policy("app.security:readonly_policy") -- Create scope with policies local scope = security.new_scope() scope = scope:with(admin_policy) scope = scope:with(readonly_policy) -- Scopes are immutable - :with() returns new scope ``` ### Named Scopes (Policy Groups) Load all policies from a group: ```lua -- Load scope with all policies in group local scope, err = security.named_scope("app.security:admin") ``` Policies are assigned to groups via the `groups` field: ```yaml - name: admin_policy kind: security.policy policy: # ... groups: - admin # This policy is in "admin" group - default # Can be in multiple groups ``` ### Scope Operations ```lua -- Add policy local new_scope = scope:with(policy) -- Remove policy local new_scope = scope:without("app.security:temp_policy") -- Check if policy is in scope local has = scope:contains("app.security:admin_policy") -- Get all policies local policies = scope:policies() ``` ### Evaluation Flow ``` 1. Check each policy in scope 2. If ANY policy returns Deny → Result is Deny 3. If at least one Allow and no Deny → Result is Allow 4. No applicable policies → Result is Undefined ``` ### Evaluation Results | Result | Meaning | |--------|---------| | `allow` | Access granted | | `deny` | Access explicitly denied | | `undefined` | No policy matched | ```lua -- Evaluate directly local result = scope:evaluate(actor, "read", "document:123", { owner = "user:456", classification = "internal" }) if result == "deny" then return nil, errors.new("FORBIDDEN", "Access denied") elseif result == "undefined" then -- No policy matched - depends on strict mode end ``` ### Quick Permission Check ```lua -- Check against current context's actor and scope local allowed = security.can("read", "document:123", { owner = "user:456" }) if not allowed then return nil, errors.new("FORBIDDEN", "Access denied") end ``` ## Token Stores Token stores provide secure token creation, validation, and revocation. ### Configuration ```yaml # src/auth/_index.yaml version: "1.0" namespace: app.auth entries: # Register environment variable - name: os_env kind: env.storage.os - name: AUTH_SECRET_KEY kind: env.variable variable: AUTH_SECRET_KEY storage: app.auth:os_env # Backing store for tokens - name: token_data kind: store.memory lifecycle: auto_start: true # Token store - name: tokens kind: security.token_store store: app.auth:token_data token_length: 32 default_expiration: "24h" token_key_env: "AUTH_SECRET_KEY" ``` ### Token Store Options | Option | Default | Description | |--------|---------|-------------| | `store` | required | Backing key-value store reference | | `token_length` | 32 | Token size in bytes (256 bits) | | `default_expiration` | 24h | Default token TTL | | `token_key` | none | HMAC-SHA256 signing key (direct value) | | `token_key_env` | none | Environment variable name for signing key | Use `token_key_env` in production to avoid embedding secrets in entries. See [Environment System](system/env.md) for registering environment variables. ### Creating Tokens ```lua local security = require("security") -- Get token store local store, err = security.token_store("app.auth:tokens") if err then return nil, err end -- Create actor and scope local actor = security.new_actor("user:123", { role = "user", email = "user@example.com" }) local scope, _ = security.named_scope("app.security:default") -- Create token local token, err = store:create(actor, scope, { expiration = "7d", -- Override default expiration meta = { device = "mobile", ip = "192.168.1.1" } }) if err then return nil, err end -- Token format: base64_token.hmac_signature (if token_key set) -- Example: "dGVzdHRva2VuMTIz.a1b2c3d4e5f6" ``` ### Validating Tokens ```lua -- Validate token local actor, scope, err = store:validate(token) if err then return nil, errors.new("UNAUTHORIZED", "Invalid token") end -- Actor and scope are reconstructed from stored data print(actor:id()) -- "user:123" ``` ### Revoking Tokens ```lua -- Revoke single token local ok, err = store:revoke(token) -- Close store when done store:close() ``` ## Context Flow Security context propagates through function calls. ### Setting Context ```lua local funcs = require("funcs") -- Call function with security context local result, err = funcs.new() :with_actor(actor) :with_scope(scope) :call("app.api:protected_endpoint", data) ``` ### Context Inheritance | Component | Inherits | |-----------|----------| | Actor | Yes - passes to child calls | | Scope | Yes - passes to child calls | | Strict mode | No - application-wide | Functions inherit caller's security context. Spawned processes start fresh. ## Service-Level Security Configure default security for services: ```yaml - name: worker_service kind: process.lua source: file://worker.lua lifecycle: auto_start: true security: actor: id: "service:worker" meta: role: worker service: true policies: - app.security:worker_policy groups: - workers ``` ## Strict Mode Enable strict mode to deny access when security context is missing: ```yaml # wippy.yaml security: strict_mode: true ``` | Mode | Missing Context | Behavior | |------|-----------------|----------| | Normal | No actor/scope | Allow (permissive) | | Strict | No actor/scope | Deny (secure default) | ## Authentication Flow Token validation in an HTTP handler: ```lua local http = require("http") local security = require("security") local function protected_handler() local req = http.request() local res = http.response() -- Extract and validate token local auth = req:header("Authorization") if not auth then return res:set_status(401):write_json({error = "Missing authorization"}) end local token = auth:gsub("^Bearer%s+", "") local store, _ = security.token_store("app.auth:tokens") local actor, scope, err = store:validate(token) if err then return res:set_status(401):write_json({error = "Invalid token"}) end -- Check permission if not security.can("api.users.read", "users") then return res:set_status(403):write_json({error = "Forbidden"}) end res:write_json({user = actor:id()}) end return { handler = protected_handler } ``` Token creation during login: ```lua local actor = security.new_actor("user:" .. user.id, {role = user.role}) local scope, _ = security.named_scope("app.security:" .. user.role) local store, _ = security.token_store("app.auth:tokens") local token, err = store:create(actor, scope, {expiration = "24h"}) ``` ## Best Practices 1. **Least privilege** - Grant minimum required permissions 2. **Deny by default** - Use explicit allow policies, enable strict mode 3. **Use policy groups** - Organize policies by role/function 4. **Sign tokens** - Always set `token_key_env` in production 5. **Short expiration** - Use shorter token lifetimes for sensitive operations 6. **Condition on context** - Use dynamic conditions over static policies 7. **Audit sensitive actions** - Log security-relevant operations ## Security Module Reference | Function | Description | |----------|-------------| | `security.actor()` | Get current actor from context | | `security.scope()` | Get current scope from context | | `security.can(action, resource, meta?)` | Check permission | | `security.new_actor(id, meta?)` | Create new actor | | `security.new_scope(policies?)` | Create empty or seeded scope | | `security.policy(id)` | Get policy by ID | | `security.named_scope(group_id)` | Get scope with all group policies | | `security.token_store(id)` | Get token store | ## Navigation Previous: Template (system/template) Next: Exec (system/exec)