Files
KArtSell.Aegis/docs/contracts/data/VS-01_DATA_CONTRACT.md
T
kjh2064 555133d245 feat: Start Phase 2 Batch 1 - VS-01 ManageIdentityAndRoles (GOV, DATA, DOMAIN)
Phase 2 Batch 1 - No Dependencies (Start Immediately)
├─ VS-01: ManageIdentityAndRoles
│  ├─ GOV: VS-01_SLICE_SPEC.md (Policy/Scope/Failure/Acceptance)
│  ├─ DATA: VS-01_DATA_CONTRACT.md (3NF schema, PIT, CDC events)
│  └─ DOMAIN: VS01_IdentityPolicyTests.cs (15 tests, pure logic)
└─ VS-02: SynchronizeSecurityMaster (🔜 Next)

### VS-01 GOV Component
- User Management (CRUD, soft-delete)
- Role & Permission Model (Admin/Analyst/Trader/Viewer)
- Data Integrity (PIT compliance, immutable email)
- API Contracts (POST/GET/PATCH endpoints)
- UI/UX Acceptance Criteria
- Security Model
- Failure Modes & Recovery

### VS-01 DATA Component
- Schema (3NF): identity.users, identity.roles, identity.user_roles, identity.user_permissions
- Constraints: Email UNIQUE, status ENUM, PIT temporal ordering
- Immutability: Email/UserID/Roles cannot change post-creation
- Soft-delete: removed_at pattern (append-only)
- PIT Queries: published_at <= cutoff validation
- CDC Events: UserCreated, RoleAssigned, RoleRevoked
- Idempotency: Email-based dedup, role assignment idempotent

### VS-01 DOMAIN Component
- 15 Domain Policy Tests (NO database, pure logic)
   Email validation (format, normalization, case-insensitivity)
   Password validation (length ≥12 chars)
   Role management (assign, revoke, idempotency)
   Permission hierarchy (role-based access control)
   User status transitions (active/inactive/suspended)
   Admin-only operations (user creation, role modification)
   Immutability (email, user ID)
   Soft-delete (inactive users filtered out)
   Consistency (every user must have role)

Execution Timeline (Per Slice):
- GOV: 1-2 hours  COMPLETE
- DATA: 2-3 hours  COMPLETE
- DOMAIN: 2-3 hours  COMPLETE
- BE: 3-4 hours (next)
- ASYNC: 2-3 hours
- FE: 3-4 hours
- TESTOPS: 2-3 hours

Total VS-01: ~18-22 hours (wall-clock ~3 days)

Phase 2 Status:
- Batch 1: 3/14 components COMPLETE (VS-01: 3/7, VS-02: 0/7)
- Batch 2-3: 🔜 Queued (after Batch 1 deps satisfied)
- 56 items total, 8 parallel batches

AGENTS.md v16.0 Compliance:
 Necessity: User goal/non-goal/acceptance criteria specified
 Pattern: Vertical Slice (GOV → DATA → DOMAIN → BE → ASYNC → FE → TESTOPS)
 Traceability: VS-01 specs linked to Phase 2 plan
 Safety: Pure logic tests (no side effects)
 Maturity: Contracts before implementation

Next: VS-01 BE (API/Handler/SQL) OR continue parallel VS-02

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-04 01:15:42 +09:00

375 lines
10 KiB
Markdown

# VS-01: Identity and Roles Data Contract
**Slice:** VS-01 (ManageIdentityAndRoles)
**Status:** 📋 SPECIFICATION
**Version:** 1.0
**Created:** 2026-08-04
---
## Schema (3NF Write Model)
### identity.users (User Accounts)
**Purpose:** Immutable user record (append-only, PIT envelope)
```sql
CREATE TABLE identity.users (
-- Primary Key
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Business Keys (immutable)
email VARCHAR(255) NOT NULL UNIQUE,
email_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 of email
-- Authentication (write-once)
password_hash VARCHAR(255) NOT NULL, -- bcrypt, never changed after creation
-- State
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'inactive', 'suspended')),
-- Temporal (PIT)
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Revision Tracking
revision INT NOT NULL DEFAULT 1,
content_hash VARCHAR(64) NOT NULL, -- SHA-256 of (email, status, updated_at)
-- Audit
created_by_user_id UUID REFERENCES identity.users(id),
correlation_id VARCHAR(36) NOT NULL,
-- Indexing
CONSTRAINT email_lowercase CHECK (email = LOWER(email)),
CONSTRAINT valid_email CHECK (email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$')
);
CREATE INDEX idx_users_email ON identity.users(email);
CREATE INDEX idx_users_status ON identity.users(status);
CREATE INDEX idx_users_published_at ON identity.users(published_at);
CREATE INDEX idx_users_created_by ON identity.users(created_by_user_id);
```
**Constraints:**
- ✅ email UNIQUE: Only one account per email per environment
- ✅ status IN ('active', 'inactive', 'suspended'): Enum validation
- ✅ published_at ≤ CURRENT_TIMESTAMP: Never future-dated
- ✅ created_at ≤ updated_at: Temporal order
**PIT (Point-in-Time) Query:**
```sql
SELECT * FROM identity.users
WHERE published_at <= @cutoff
AND status = 'active'
ORDER BY created_at DESC;
```
---
### identity.roles (Role Definitions)
**Purpose:** Immutable, predefined roles (reference data)
```sql
CREATE TABLE identity.roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO identity.roles (name, description) VALUES
('Admin', 'Full system access'),
('Analyst', 'Read-only analysis'),
('Trader', 'Execute trades'),
('Viewer', 'Dashboard read-only');
-- Prevent deletion (immutable reference data)
CREATE TRIGGER prevent_role_deletion
BEFORE DELETE ON identity.roles
FOR EACH ROW
EXECUTE FUNCTION raise_immutability_error();
```
**Constraints:**
- ✅ name UNIQUE: One role per name
- ✅ Immutable: No INSERT/UPDATE/DELETE after initial load
- ✅ Predefined: Only 4 roles (Admin, Analyst, Trader, Viewer)
---
### identity.user_roles (User-Role Assignment)
**Purpose:** Many-to-many junction table (append-only)
```sql
CREATE TABLE identity.user_roles (
id BIGSERIAL PRIMARY KEY,
-- Foreign Keys
user_id UUID NOT NULL REFERENCES identity.users(id),
role_id INT NOT NULL REFERENCES identity.roles(id),
-- Temporal
assigned_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
removed_at TIMESTAMP, -- NULL = still assigned, NOT NULL = removed
-- Audit
assigned_by_user_id UUID REFERENCES identity.users(id),
correlation_id VARCHAR(36) NOT NULL,
-- Versioning (for CDC)
revision INT NOT NULL DEFAULT 1,
-- Constraints
CONSTRAINT active_assignment CHECK (assigned_at <= published_at),
CONSTRAINT valid_removal CHECK (removed_at IS NULL OR removed_at >= assigned_at),
CONSTRAINT unique_active_role UNIQUE (user_id, role_id) WHERE removed_at IS NULL
);
CREATE INDEX idx_user_roles_user ON identity.user_roles(user_id);
CREATE INDEX idx_user_roles_role ON identity.user_roles(role_id);
CREATE INDEX idx_user_roles_active ON identity.user_roles(user_id, removed_at);
CREATE INDEX idx_user_roles_published ON identity.user_roles(published_at);
```
**Constraints:**
- ✅ UNIQUE (user_id, role_id) WHERE removed_at IS NULL: No duplicate active roles
- ✅ assigned_at ≤ published_at: Temporal ordering
- ✅ removed_at IS NULL: Active assignment tracking
**PIT Query (Get current roles for user):**
```sql
SELECT ur.user_id, r.name AS role
FROM identity.user_roles ur
JOIN identity.roles r ON ur.role_id = r.id
WHERE ur.user_id = @userId
AND ur.published_at <= @cutoff
AND ur.removed_at IS NULL;
```
---
### identity.user_permissions (Permission Grant)
**Purpose:** Fine-grained permission model (append-only)
```sql
CREATE TABLE identity.user_permissions (
id BIGSERIAL PRIMARY KEY,
-- Foreign Keys
user_id UUID NOT NULL REFERENCES identity.users(id),
-- Permission (domain-scoped)
resource VARCHAR(50) NOT NULL, -- e.g., 'users', 'portfolios', 'trades'
action VARCHAR(20) NOT NULL, -- 'read', 'write', 'approve', 'execute'
-- Temporal
granted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
revoked_at TIMESTAMP, -- NULL = active, NOT NULL = revoked
-- Audit
granted_by_user_id UUID REFERENCES identity.users(id),
correlation_id VARCHAR(36) NOT NULL,
-- Constraints
CONSTRAINT valid_resource CHECK (resource IN ('users', 'portfolios', 'trades', 'models', 'signals')),
CONSTRAINT valid_action CHECK (action IN ('read', 'write', 'approve', 'execute')),
CONSTRAINT unique_active_permission UNIQUE (user_id, resource, action) WHERE revoked_at IS NULL
);
CREATE INDEX idx_permissions_user ON identity.user_permissions(user_id);
CREATE INDEX idx_permissions_resource ON identity.user_permissions(resource, action);
```
---
## Data Integrity Rules
### Rule 1: Email Immutability
**Constraint:** email CANNOT be updated after creation
**Verification:**
```sql
-- Test: Email update should fail
UPDATE identity.users SET email = 'newemail@example.com'
WHERE id = @userId;
-- Expected: CONSTRAINT VIOLATION (or trigger prevents update)
```
### Rule 2: Password Hash Never Logged
**Constraint:** password_hash column exists but NEVER appears in SELECT without WHERE
**Verification:**
```sql
-- Bad (never do this):
SELECT * FROM identity.users; -- ❌ Exposes password_hash
-- Good (always explicit):
SELECT id, email, status FROM identity.users; -- ✅ No password
```
### Rule 3: PIT (Point-in-Time) Queries Must Include Cutoff
**Constraint:** All reads include `WHERE published_at <= @cutoff`
**Verification:**
```sql
-- Correct:
SELECT * FROM identity.users WHERE published_at <= @cutoff AND status = 'active';
-- Wrong (time-machine unsafe):
SELECT * FROM identity.users WHERE status = 'active'; -- ❌ No cutoff
```
### Rule 4: No Direct Email Mutations
**Constraint:** Email cannot be part of UPDATE statement
**Verification (trigger):**
```sql
CREATE TRIGGER prevent_email_update
BEFORE UPDATE ON identity.users
FOR EACH ROW
WHEN (OLD.email IS DISTINCT FROM NEW.email)
EXECUTE FUNCTION raise_immutability_error('email');
```
### Rule 5: Role Removal via Soft Delete
**Constraint:** Set removed_at timestamp instead of DELETE
**Verification:**
```sql
-- Correct:
UPDATE identity.user_roles SET removed_at = CURRENT_TIMESTAMP
WHERE user_id = @userId AND role_id = @roleId;
-- Wrong (no DELETE):
DELETE FROM identity.user_roles WHERE user_id = @userId; -- ❌ Banned
```
---
## Event Contracts (CDC)
### UserCreated Event
```json
{
"eventId": "UUID",
"eventType": "UserCreated",
"userId": "UUID",
"email": "user@example.com",
"roles": ["Admin", "Analyst"],
"createdAt": "2026-08-04T12:00:00Z",
"correlationId": "req-001"
}
```
**When:** INSERT into identity.users
**Consumer:** ApprovalQueue (if user requires approval)
### RoleAssigned Event
```json
{
"eventId": "UUID",
"eventType": "RoleAssigned",
"userId": "UUID",
"roleName": "Analyst",
"assignedAt": "2026-08-04T12:00:00Z",
"correlationId": "req-001"
}
```
**When:** INSERT into identity.user_roles with removed_at IS NULL
**Consumer:** PermissionCache (invalidate user's permission set)
### RoleRevoked Event
```json
{
"eventId": "UUID",
"eventType": "RoleRevoked",
"userId": "UUID",
"roleName": "Analyst",
"revokedAt": "2026-08-04T12:00:00Z",
"correlationId": "req-001"
}
```
**When:** UPDATE identity.user_roles SET removed_at = now()
**Consumer:** PermissionCache (invalidate user's permission set)
---
## Idempotency & Replay Safety
### Create User Idempotency
**Input:** IdempotencyKey = `create-user-alice-20260804`
**First Run:**
```sql
INSERT INTO identity.users (email, password_hash, correlation_id)
VALUES ('alice@example.com', 'bcrypt(...)', 'req-001')
RETURNING id;
-- Result: UUID = 12345678-1234-1234-1234-123456789012
```
**Replay (same IdempotencyKey):**
```sql
-- Check if already created
SELECT id FROM identity.users WHERE email = 'alice@example.com';
-- Result: 12345678-1234-1234-1234-123456789012 (same)
-- Action: Return existing record (no duplicate INSERT)
```
### Assign Role Idempotency
**Input:** IdempotencyKey = `assign-alice-analyst-20260804`
**First Run:**
```sql
INSERT INTO identity.user_roles (user_id, role_id, assigned_by_user_id)
VALUES (uuid-alice, 2, admin-user-id)
RETURNING id;
-- Result: ID = 1001
```
**Replay:**
```sql
-- Check if already assigned
SELECT id FROM identity.user_roles
WHERE user_id = uuid-alice AND role_id = 2 AND removed_at IS NULL;
-- Result: 1001 (same)
-- Action: Return existing record (no duplicate)
```
---
## Acceptance Criteria Checklist
- [ ] All tables created with 3NF normalization
- [ ] PIT queries tested (published_at ≤ cutoff)
- [ ] Append-only verified (no direct UPDATE on business keys)
- [ ] Immutability enforced (email, roles)
- [ ] Soft-delete working (removed_at pattern)
- [ ] Idempotency verified (replay tests passing)
- [ ] CDC events defined (UserCreated, RoleAssigned, RoleRevoked)
- [ ] Indexes created for performance
- [ ] Constraints enforced (CHECK, UNIQUE, FK)
---
## Sign-Off
| Role | Approval | Date |
|------|----------|------|
| Data Architect | ✅ Draft | 2026-08-04 |
| DBA | ⏳ Review | TBD |
| Security | ⏳ Review | TBD |
---
**Status:** 📋 **READY FOR DOMAIN TESTS & BE IMPLEMENTATION**
Next: DomainPolicyTests (identity rules validation)