# VS-02: Security Master Data Contract

**Slice:** VS-02 (SynchronizeSecurityMaster)  
**Status:** 📋 SPECIFICATION  
**Version:** 1.0  
**Created:** 2026-08-04  

---

## Schema (3NF Write Model)

### security.rules (Permission Rules)

```sql
CREATE TABLE security.rules (
    id SERIAL PRIMARY KEY,
    rule_name VARCHAR(100) NOT NULL UNIQUE,
    resource VARCHAR(50) NOT NULL,  -- 'users', 'portfolios', 'trades'
    action VARCHAR(20) NOT NULL,    -- 'read', 'write', 'execute'
    description VARCHAR(255),
    
    -- Temporal & Versioning
    version INT NOT NULL DEFAULT 1,
    effective_at TIMESTAMP NOT NULL,
    expires_at TIMESTAMP,
    published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    -- Audit
    created_by_user_id UUID,
    correlation_id VARCHAR(36),
    
    -- Constraints
    CONSTRAINT valid_resource CHECK (resource IN ('users', 'portfolios', 'trades', 'models')),
    CONSTRAINT valid_action CHECK (action IN ('read', 'write', 'execute', 'approve')),
    CONSTRAINT temporal_order CHECK (effective_at <= published_at),
    UNIQUE(rule_name, version)
);

CREATE INDEX idx_rules_effective_published 
    ON security.rules(effective_at, published_at);
```

### security.role_permissions (Role-Permission Mapping)

```sql
CREATE TABLE security.role_permissions (
    id BIGSERIAL PRIMARY KEY,
    role_id INT NOT NULL REFERENCES identity.roles(id),
    rule_id INT NOT NULL REFERENCES security.rules(id),
    
    -- Temporal
    assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    removed_at TIMESTAMP,  -- Soft delete
    
    -- Audit
    correlation_id VARCHAR(36),
    
    -- Constraints
    CONSTRAINT valid_removal CHECK (removed_at IS NULL OR removed_at >= assigned_at),
    UNIQUE(role_id, rule_id) WHERE removed_at IS NULL
);

CREATE INDEX idx_role_perms_active 
    ON security.role_permissions(role_id, removed_at);
```

### security.access_control_rules (Conditional Rules)

```sql
CREATE TABLE security.access_control_rules (
    id BIGSERIAL PRIMARY KEY,
    rule_id INT NOT NULL REFERENCES security.rules(id),
    
    -- Condition
    condition_type VARCHAR(50) NOT NULL,  -- 'time-based', 'location-based', 'mfa-required'
    condition_value JSONB NOT NULL,       -- {"startTime": "09:30", "endTime": "16:00"}
    
    -- Temporal
    effective_at TIMESTAMP NOT NULL,
    expires_at TIMESTAMP,
    published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT valid_condition_type CHECK (condition_type IN ('time-based', 'location-based', 'mfa-required'))
);
```

### security.sync_checkpoint (Sync History)

```sql
CREATE TABLE security.sync_checkpoint (
    id BIGSERIAL PRIMARY KEY,
    
    -- Sync State
    sync_version INT NOT NULL UNIQUE,  -- Incremental version
    total_rules INT NOT NULL,
    synced_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    -- Idempotency
    correlation_id VARCHAR(36) UNIQUE,
    
    -- Status
    status VARCHAR(20) DEFAULT 'success'  -- 'success', 'partial', 'failed'
        CHECK (status IN ('success', 'partial', 'failed')),
    
    -- Rollback
    previous_version INT REFERENCES security.sync_checkpoint(sync_version),
    error_message VARCHAR(500)
);

CREATE INDEX idx_sync_latest ON security.sync_checkpoint(synced_at DESC);
```

---

## PIT (Point-in-Time) Queries

**Get current permissions for role:**
```sql
SELECT sr.rule_name, sr.resource, sr.action
FROM security.role_permissions rp
JOIN security.rules sr ON rp.rule_id = sr.id
WHERE rp.role_id = @roleId
  AND rp.published_at <= @cutoff
  AND rp.removed_at IS NULL
  AND sr.effective_at <= @cutoff
  AND (sr.expires_at IS NULL OR sr.expires_at > @cutoff);
```

**Get rules active at specific time:**
```sql
SELECT * FROM security.rules
WHERE published_at <= @cutoff
  AND effective_at <= @cutoff
  AND (expires_at IS NULL OR expires_at > @cutoff);
```

---

## CDC Events

### SecurityMasterSynced

```json
{
  "eventId": "UUID",
  "eventType": "SecurityMasterSynced",
  "syncVersion": 42,
  "totalRules": 156,
  "newRules": 3,
  "modifiedRules": 5,
  "syncedAt": "2026-08-04T12:00:00Z",
  "correlationId": "sync-001"
}
```

### PermissionRuleUpdated

```json
{
  "eventId": "UUID",
  "eventType": "PermissionRuleUpdated",
  "ruleId": 123,
  "ruleName": "trader_execute_permission",
  "action": "execute",
  "version": 2,
  "syncVersion": 42,
  "correlationId": "sync-001"
}
```

---

## Acceptance Criteria Checklist

- [ ] All tables created with 3NF normalization
- [ ] PIT queries tested (published_at, effective_at, expires_at)
- [ ] Append-only verified (no direct UPDATE on business keys)
- [ ] Soft-delete working (removed_at pattern)
- [ ] Sync checkpoint tracked (version-based idempotency)
- [ ] CDC events defined (SecurityMasterSynced, PermissionRuleUpdated)
- [ ] Conditional rules supported (time-based, location-based, MFA)
- [ ] Indexes created for performance

---

**Status:** 📋 **READY FOR DOMAIN TESTS & BE IMPLEMENTATION**

Next: VS-02 DOMAIN Tests (sync logic validation)
