-
Notifications
You must be signed in to change notification settings - Fork 2
Architecture and Design
This page describes the system architecture, component design, data models, and authentication flows for the TMI (Threat Modeling Improved) system.
TMI consists of two main applications:
| Component | Technology | Repository | Description |
|---|---|---|---|
| tmi | Go / Gin | ericfitz/tmi | Backend API server providing RESTful endpoints, WebSocket collaboration, OAuth authentication, and multi-database (PostgreSQL, MySQL, SQLite, SQL Server, Oracle) / Redis data storage |
| tmi-ux | TypeScript / Angular | ericfitz/tmi-ux | Frontend web application providing the user interface, DFD diagram editor (AntV/X6), and client-side session management |
Throughout this page, sections are labeled to indicate which component they apply to:
- [Backend: tmi] - Go server-side implementation
- [Frontend: tmi-ux] - Angular client-side implementation
- [Both] - Applies to both components or describes their interaction
- System Overview
- Architecture Patterns
- Component Architecture
- Data Architecture [Backend: tmi]
- Authentication and Authorization [Both]
- Collaboration Architecture [Both]
- Security Design [Both]
- Scalability Architecture [Backend: tmi]
- Deployment Architecture [Backend: tmi]
- Implementation Reference
- Source Repository Reference Materials
- Validation Framework [Frontend: tmi-ux]
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ tmi-ux │◄──►│ Load Balancer │◄──►│ tmi │
│ (Angular/TS) │ │ (Nginx/HAProxy)│ │ (Go/Gin) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ OAuth/SAML IdPs │◄──►│ Authentication │◄──►│ RDBMS │
│ (Configurable) │ │ & JWT Layer │ │ Primary DB │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ WebSocket Hub │◄──►│ Redis Cache │◄──►│ Monitoring │
│ (Collaboration) │ │ (Sessions/RT) │ │ & Logging │
└─────────────────┘ └──────────────────┘ └─────────────────┘
The Go backend API server provides:
- HTTP API: RESTful endpoints for CRUD operations
- WebSocket Hub: Real-time collaboration coordination
- Authentication: OAuth integration with JWT tokens
- Authorization: Role-based access control (RBAC)
- Business Logic: Threat modeling and diagram management
The Angular web application provides:
- User Interface: Responsive single-page application
- DFD Editor: AntV/X6-based diagram editor with collaborative editing
- Session Management: Client-side token handling and refresh
- Offline Capabilities: Local state management and auto-save
- RDBMS: Primary data storage for persistent entities (PostgreSQL, MySQL, SQLite, SQL Server, or Oracle)
- Redis: Session storage and real-time coordination
- File Storage: Static assets and uploaded content
- OAuth Providers: Dynamically configured via config, environment, or database (e.g., Google, GitHub, Microsoft)
- SAML Providers: Enterprise SSO integration with configurable SAML providers
- Monitoring Stack: Observability and alerting systems
The tmi backend follows DDD principles to organize business logic:
Entities: Core business objects with unique identity
-
ThreatModel- Container for security analysis -
Diagram- Data flow diagrams -
Threat- Identified security risks -
User- Authenticated user accounts
Value Objects: Immutable data containers
-
Authorization- Access control entry (principal_type, provider_id, provider, role) -
Metadata- Key-value metadata pair (entity_type, entity_id, key, value) -
Cell- Diagram cell data (id, shape, data) --- stored as JSON within Diagram
Aggregates: Consistency boundaries
- ThreatModel aggregate contains Diagrams, Threats, Assets, Documents, Notes, Repositories
- Diagram aggregate contains Cells (stored as JSON array within the Diagram entity)
Stores: Data access abstraction (interface + GORM implementation pattern)
-
ThreatModelStoreInterface/GormThreatModelStore- Threat model persistence -
DiagramStoreInterface/GormDiagramStore- Diagram persistence -
UserStore/GormUserStore- User account management - Additional stores for Assets, Documents, Notes, Repositories, Threats, Metadata, etc.
Services: Business logic coordination
-
auth.Service- OAuth and JWT management (auth/service.go) - Authorization middleware and utilities (
api/middleware.go,api/auth_utils.go) -
WebSocketHub- WebSocket collaboration coordination (api/websocket.go)
Core Domain: Business logic independent of external concerns
- Located in
/apiand/internalpackages - No dependencies on frameworks or external systems
Ports: Interfaces for external communication
-
Providerinterface for OAuth providers (auth/provider.go) -
ThreatModelStoreInterface,DiagramStoreInterface, etc. for persistence (api/store.go) -
ProviderRegistryinterface for provider configuration (auth/provider_registry.go)
Adapters: Implementations of external integrations
- Dynamically configured OAuth providers via
ProviderRegistry(config, environment, or database) -
GormThreatModelStore,GormDiagramStore, etc. - GORM-based persistence (PostgreSQL, MySQL, SQLite, SQL Server, Oracle) - Redis-backed ticket store, state store, and cache
Dependency Inversion: The core depends on abstractions, not concrete implementations.
Command-Query Separation: Clear separation between reads and writes.
- Commands: POST, PUT, PATCH, DELETE operations
- Queries: GET operations
Domain Events: Business event notifications.
ThreatModelCreatedDiagramUpdatedUserJoinedCollaboration
Real-time Events: WebSocket-based real-time updates.
-
diagram_operation- Diagram changes -
presenter_cursor- Cursor position in presenter mode -
join/leave- Collaboration session events
The tmi-ux frontend uses automated tooling to enforce architecture boundaries and validate design decisions.
The tmi-ux project uses ESLint rules (defined in eslint.config.js) to enforce architecture boundaries automatically.
Core Layer Isolation (src/app/core/**):
- Core services must not import from feature modules (
pages/*,auth/services/*). - Use interfaces in
core/interfaces/for cross-layer communication instead.
Domain Layer Purity (src/app/**/domain/**):
- Domain objects must not depend on Angular (
@angular/core,@angular/common). - Domain objects must not depend on RxJS (
rxjs,rxjs/*). - Domain objects must not depend on infrastructure, application, or service layers.
- Domain objects must not depend on third-party framework libraries (
@antv/*,@jsverse/*). - Keep domain objects as pure TypeScript classes with business logic only.
Import Restrictions:
- Do not import from
*.modulefiles (use standalone components instead). - Do not import the entire
@angular/materiallibrary (import specific modules). - Prefer
@app/shared/importsfor common imports.
Service Provisioning:
- Root services use
providedIn: 'root'. - Feature services are provided in the component
providersarray.
# Run all linting including architecture rules
pnpm run lint:all
# Run only TypeScript/architecture linting
pnpm run lint
# Fix auto-fixable issues
pnpm run lint:all --fixUse madge to detect and visualize circular dependencies:
# Install madge globally
npm install -g madge
# Check for circular dependencies
madge --circular src/
# Generate visual dependency graph
madge --image graph.svg src/| Layer | Allowed Dependencies |
|---|---|
| Core | Must not depend on features |
| Features | May depend on core |
| Domain | No external dependencies (pure TypeScript) |
| Infrastructure | May depend on domain and core |
Track these metrics to monitor architecture health:
| Metric | Target |
|---|---|
| Circular Dependencies | 0 |
| Core to Feature Imports | 0 |
| Domain to Framework Imports | 0 |
| Domain Services with Angular/RxJS | 0 |
| NgModule Files (except third-party) | 0 |
For complete validation procedures and troubleshooting, see the TMI-UX source repository at docs/migrated/reference/architecture/validation.md.
Handler Pattern:
// api/threat_model_handlers.go
type ThreatModelHandler struct {
wsHub *WebSocketHub
}
func (h *ThreatModelHandler) CreateThreatModel(c *gin.Context) {
// 1. Parse request
// 2. Validate input
// 3. Apply business logic
// 4. Persist to database
// 5. Return response
}Key Handlers:
-
threat_model_handlers.go- Threat model CRUD -
threat_model_diagram_handlers.go- Diagram operations within threat models -
websocket_handlers.go,websocket_session_handlers.go,websocket_presenter_handlers.go- WebSocket collaboration management - Authentication handlers are in
/auth/handlers.go,/auth/handlers_oauth.go,/auth/handlers_saml.go, etc.
OAuth Flow:
// auth/provider.go
type Provider interface {
GetOAuth2Config() *oauth2.Config
GetAuthorizationURL(state string) string
ExchangeCode(ctx context.Context, code string) (*TokenResponse, error)
GetUserInfo(ctx context.Context, accessToken string) (*UserInfo, error)
ValidateIDToken(ctx context.Context, idToken string) (*IDTokenClaims, error)
}Providers (dynamically configured via config, environment, or database):
- OAuth providers (e.g., Google, GitHub, Microsoft) registered via
ProviderRegistry - SAML providers for enterprise SSO
- Test provider (development only)
JWT Management:
// auth/service.go
type Claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified,omitempty"`
Name string `json:"name"`
IdentityProvider string `json:"idp,omitempty"`
Groups []string `json:"groups,omitempty"`
IsAdministrator *bool `json:"tmi_is_administrator,omitempty"`
IsSecurityReviewer *bool `json:"tmi_is_security_reviewer,omitempty"`
jwt.RegisteredClaims
}Store Pattern:
// Interface-based store abstraction (api/store.go)
type ThreatModelStoreInterface interface {
Get(id string) (ThreatModel, error)
GetIncludingDeleted(id string) (ThreatModel, error)
GetAuthorization(id string) ([]Authorization, User, error)
List(offset, limit int, filter func(ThreatModel) bool) []ThreatModel
ListWithCounts(offset, limit int, filter func(ThreatModel) bool, filters *ThreatModelFilters) ([]TMListItem, int)
Create(item ThreatModel, idSetter func(ThreatModel, string) ThreatModel) (ThreatModel, error)
Update(id string, item ThreatModel) error
Delete(id string) error
}
// Global store instances
var ThreatModelStore ThreatModelStoreInterface
var DiagramStore DiagramStoreInterface
// ... additional stores for Assets, Documents, Notes, Repositories, Threats, Metadata, etc.
// GORM-backed implementation (api/database_store_gorm.go)
type GormThreatModelStore struct {
db *gorm.DB
mutex sync.RWMutex
}Benefits:
- Interface-based abstraction allows swapping implementations.
- GORM-backed stores support PostgreSQL, MySQL, SQLite, SQL Server, and Oracle.
- Mutexes provide concurrency protection.
- Each entity type has a clearly separated store.
API Service (core/services/api.service.ts):
@Injectable({providedIn: 'root'})
export class ApiService {
private apiUrl = environment.apiUrl;
getThreatModels(): Observable<ThreatModel[]> {
return this.http.get<ThreatModel[]>(`${this.apiUrl}/threat_models`);
}
}Logger Service (core/services/logger.service.ts):
export class LoggerService {
private logLevel: LogLevel;
debug(message: string, ...args: any[]): void {
if (this.shouldLog(LogLevel.DEBUG)) {
console.log(`[DEBUG] ${this.timestamp()} ${message}`, ...args);
}
}
}Authentication Service (core/services/auth.service.ts):
- OAuth flow management
- JWT token storage
- Session management
- User profile retrieval
Threat Model Page (pages/tm/):
- List view of threat models
- Create/edit threat models
- Access control management
Diagram Editor (pages/dfd/):
- AntV/X6-based diagram editor
- WebSocket-based collaboration
- Real-time synchronization
The DFD editor follows a layered architecture with clear separation of concerns.
State Management Layer (pages/dfd/application/services/):
-
AppStateService- Orchestrates diagram state, including sync status, remote operations, and conflict resolution. -
AppWebSocketEventProcessor- Processes incoming WebSocket events and emits typed application-level events. -
AppOperationStateManager- Manages operation state flags and drag tracking for history coordination.
Key Patterns:
-
Event-based Communication: Services communicate through Observable event streams rather than bidirectional dependencies.
AppOperationStateManageremitsstateEvents$thatAppStateServicesubscribes to, avoiding circular dependencies. -
Processed Event Interfaces: Raw WebSocket events are transformed into typed application events (
ProcessedDiagramOperation,ProcessedDiagramSync, etc.) before consumption by the state layer. - Single Responsibility: Event processing is separated from state management, enabling independent testing and maintenance.
For implementation details, see the source repository: docs/migrated/refactoring/app-state-service-refactor-plan.md.
DFD Change Propagation Architecture:
The DFD component implements a change propagation system for handling user interactions, collaborative editing, auto-save, and visual effects. The system has evolved into a complex architecture with:
- 15+ services involved in change propagation.
- 20+ decision points that affect flow.
- 4 different pathways for node creation.
- 3 state stores requiring synchronization.
- 2,274+ lines of coordination logic in the main DfdComponent.
Architecture Strengths:
- Robust collaborative editing with real-time synchronization.
- History management that excludes visual-only changes.
- Comprehensive auto-save system that prevents data loss.
- Flexible visual effects that provide immediate user feedback.
Known Architectural Issues:
- Multiple Code Paths: Similar operations can follow different code paths depending on context.
- Scattered Orchestration: Logic is spread across the DfdComponent and multiple services.
- Complex State Management: Overlapping responsibilities between stores.
- Permission Checking Complexity: Race conditions and unclear fallback logic.
- Auto-save Logic Duplication: Auto-save logic is repeated across different event sources.
Change Propagation Patterns:
The system implements four distinct patterns for propagating changes:
| Pattern | Flow | Use Case |
|---|---|---|
| Local User Actions | X6 Events -> History System -> Auto-save | Direct user interactions |
| Collaborative Mode | X6 Events -> Diagram Operation Broadcast -> WebSocket | Multi-user editing |
| Remote Operations | WebSocket -> State Service -> Direct Graph Updates | Receiving remote changes |
| Visual Effects | Visual Effects Service -> Direct Styling (history excluded) | Non-semantic visual feedback |
Developer Guidelines:
- DFD features: Review the user actions flow to understand current behavior, check which systems your changes will affect, and consider the impact on both solo and collaborative modes.
- Collaborative editing: Understand WebSocket message handling, review permission checking complexity, and study state synchronization patterns.
- Auto-save and persistence: Understand the auto-save triggering logic, review history filtering mechanisms, and consider performance implications.
For comprehensive documentation including Mermaid flow diagrams, see the source repository at docs/migrated/reference/architecture/dfd-change-propagation/.
Visual Effects Pipeline (pages/dfd/infrastructure/services/):
The visual effects system provides immediate visual feedback for user interactions without interfering with semantic change propagation or the history system. Key services:
-
InfraVisualEffectsService- Manages creation highlights with fade-out animations. -
InfraX6SelectionAdapter- Handles X6-specific selection implementation and visual effects. -
InfraPortStateService- Manages port visibility state and connection tracking. -
DfdStateStore- Local UI state store (cells$,selectedNode$,canUndo$,canRedo$,isLoading$).
Visual Effect Types:
| Effect Type | Trigger | Implementation |
|---|---|---|
| Selection Effects | User selects cells |
InfraX6SelectionAdapter.applySelectionEffect() applies red glow filter and stroke styling |
| Hover Effects | Mouse enters/leaves cell |
InfraX6SelectionAdapter.applyHoverEffect() applies brightness filter |
| Creation Highlights | Programmatic cell creation |
InfraVisualEffectsService.applyCreationHighlight() with 500ms fade animation |
| Port Visibility | Connection interactions |
InfraPortStateService shows/hides ports based on connections and hover state |
History Suppression: Visual effects use AppOperationStateManager.executeVisualEffect() to execute styling changes without polluting the undo/redo history.
Styling Constants (from DFD_STYLING in pages/dfd/constants/styling-constants.ts):
- Selection: Red glow (
rgba(255, 0, 0, 0.8)), 8px blur, stroke color#000000, stroke width 2px - Hover: Red glow (
rgba(255, 0, 0, 0.6)), 4px blur - Creation: Blue glow (
rgba(0, 150, 255, 0.9)), 12px blur, 500ms fade duration
State Synchronization: The system manages multiple state stores:
-
DfdStateStore- Local UI state (cells$,selectedNode$,canUndo$,canRedo$,isLoading$). -
AppStateService- Collaborative state (pendingOperations,syncState,conflicts). - X6 Graph - Source of truth for diagram structure.
For detailed diagrams and flow documentation, see the source repository: docs/migrated/reference/architecture/dfd-change-propagation/visual-effects-pipeline.md
Unified Operation Flow Layer (pages/dfd/application/):
The DFD editor uses a unified operation flow architecture where all graph modifications flow through a single, well-defined pipeline. This approach ensures consistency, enables proper history tracking, supports real-time collaboration, and provides a single point for persistence operations.
Key Principles:
-
Single Source of Truth: All changes flow through
AppGraphOperationManager. - Source Awareness: Every operation knows its origin (user, remote, load, undo/redo).
- Converged Pipeline: Maximize code reuse across different operation sources.
- History at the Right Level: Track graph-interaction-level operations, not low-level X6 events.
- Coordinated Side Effects: History, broadcasting, and auto-save are coordinated based on operation source.
Operation Sources:
| Source | Description | Record History? | Broadcast? | Auto-Save? |
|---|---|---|---|---|
user-interaction |
Direct user actions in the UI | Yes | If in collaboration session | If NOT in session |
remote-collaboration |
Operations from other users via WebSocket | No | No (already broadcast) | No (handled by remote) |
diagram-load |
Loading diagram from server/storage | No | No | No |
undo-redo |
Undo/redo operations from history | No | If in session | If NOT in session |
auto-correction |
System corrections (e.g., z-order fixes) | No | Maybe | Maybe |
Unified Operation Pipeline:
┌────────────────────────────────────────────────────────────┐
│ OPERATION SOURCES │
├─────────────┬──────────────┬──────────────┬────────────────┤
│ User Action │ Diagram Load │ Remote Ops │ Undo/Redo │
│ (UI Events) │ (REST/WS) │ (WebSocket) │ (History Svc) │
└──────┬──────┴──────┬───────┴──────┬───────┴────────┬───────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────────────────────────────┐
│ CONVERT TO GraphOperation │
│ - NodeOperations: create-node, update-node, delete-node │
│ - EdgeOperations: create-edge, update-edge, delete-edge │
│ - BatchOperations: batch of multiple operations │
│ - LoadOperations: load-diagram with full cell array │
└───────────────────────────┬────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ AppGraphOperationManager.execute() │
│ - Routes operation to appropriate executor │
│ - Provides OperationContext with source, flags, metadata │
│ - Manages operation lifecycle and error handling │
└───────────────────────────┬────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ OPERATION EXECUTORS │
│ - NodeOperationExecutor: Handles node operations │
│ - EdgeOperationExecutor: Handles edge operations │
│ - BatchOperationExecutor: Handles batch operations │
│ - LoadDiagramExecutor: Handles diagram loading │
└───────────────────────────┬────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE SERVICES │
│ - InfraNodeService: Creates/modifies nodes in X6 │
│ - InfraEdgeService: Creates/modifies edges in X6 │
│ - InfraX6GraphAdapter: Low-level X6 graph operations │
└───────────────────────────┬────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ POST-OPERATION PROCESSING │
│ - Operation completed successfully │
│ - OperationResult returned with affected cell IDs │
│ - History recorded (if user-interaction) │
│ - Broadcast/auto-save triggered (based on session mode) │
└────────────────────────────────────────────────────────────┘
Service Responsibilities:
| Service | Role | Does NOT |
|---|---|---|
AppDfdOrchestrator |
High-level coordination and public API | Directly modify graph, handle history, manage persistence |
AppGraphOperationManager |
Operation routing and lifecycle management | Know about history, broadcasting, or persistence |
| Operation Executors | Execute specific operation types via infrastructure | Record history, broadcast, or trigger persistence |
AppOperationStateManager |
Manage operation state flags and drag tracking | Modify graph or record history directly |
AppHistoryService |
Custom undo/redo history management | Trigger persistence directly |
AppRemoteOperationHandler |
Handle operations from remote collaboration | Broadcast back (would create infinite loop) |
AppPersistenceCoordinator |
Save diagram to server or local storage | Modify graph state |
State Flags:
AppOperationStateManager manages the following flags:
-
isApplyingRemoteChange- Suppresses history and broadcast. -
isDiagramLoading- Suppresses history and broadcast. -
isUndoRedoOperation- Suppresses history; allows broadcast and save.
Benefits of Unified Architecture:
- Consistency: All operations follow the same path with no special cases.
- Testability: A single pipeline to test with clear mock boundaries.
- Observability: All operations emit events for easy tracking.
- Extensibility: You add new operation types by implementing executors.
- Collaboration Support: Remote operations integrate seamlessly.
For complete implementation details including code examples and history entry format, see the source repository: docs/migrated/reference/architecture/unified-operation-flow.md
History Tracking Patterns (pages/dfd/application/):
The DFD editor implements a custom GraphOperation history system for undo/redo functionality, replacing the X6 built-in history plugin. The system uses three distinct patterns.
Retroactive Pattern - Used when X6 creates elements through user interaction (dragging to create nodes or edges):
- X6 creates the element and fires an event (e.g.,
node:added,edge:connected). - The event handler validates the element and creates a retroactive GraphOperation with
metadata.retroactive = true. - The operation executor detects that the element already exists and captures its state for history.
Implementation files: app-edge.service.ts (handleEdgeAdded()), app-dfd.facade.ts (handleNodeAdded()), edge-operation-executor.ts / node-operation-executor.ts.
Drag Completion Pattern - Used for operations with multiple interim events (movement, resizing, vertices):
- X6 fires continuous events during the drag.
-
AppOperationStateManagertracks drag state;dragCompleted$fires on mouseup. - The drag completion handler creates a GraphOperation with before/after state (only the final state is recorded).
Implementation files: app-dfd.facade.ts (handleDragCompletion(), _handleNodeMove(), _handleNodeResize(), _handleEdgeVerticesDrag()).
Direct Operation Pattern - Used for explicit user actions (delete, cut, paste):
- The user triggers an action; code creates a GraphOperation before modifying X6.
- The operation executes, modifying X6 and recording the change in history.
Implementation files: app-dfd.facade.ts (deleteSelectedCells(), cut(), copy(), paste()).
Supported Operations:
- Node: Creation (retroactive), Movement (drag), Resizing (drag), Deletion (direct), Label editing (direct), Embedding (event)
- Edge: Creation (retroactive), Deletion (direct), Label editing (direct), Vertices drag (drag), Reconnection (event)
- Clipboard: Cut (direct), Paste (retroactive), Copy (no history)
Key Services:
| Service | Responsibility |
|---|---|
AppGraphOperationManager |
Executes GraphOperations and manages operation pipeline |
AppOperationStateManager |
Tracks drag state and coordinates history operations |
AppHistoryService |
Manages undo/redo stack |
NodeOperationExecutor / EdgeOperationExecutor
|
Handle create/update/delete operations |
Excluded from History: Z-order changes, metadata changes, and data asset assignments are intentionally excluded from history tracking.
For implementation details, see the source repository: docs/migrated/development/HISTORY_TRACKING_IMPLEMENTATION.md
User Actions Flow:
User interactions with the DFD graph follow different propagation paths depending on whether the user is in collaborative mode or solo editing mode. The system filters changes to ensure that only semantic changes trigger saves and broadcasts.
Key Services and Their Roles:
| Service | Role |
|---|---|
AppDfdFacade |
Single entry point for most graph operations (node/edge creation, deletion, clipboard) |
AppEventHandlersService |
Handles keyboard events, context menu, and user interactions |
InfraWebsocketCollaborationAdapter |
Broadcasts changes via WebSocket in collaborative mode |
AppOperationStateManager |
Controls operation state flags, drag tracking, and coordinates with history |
AppHistoryService |
Manages undo/redo stacks and history entries |
AppDiagramService |
Handles saving and loading diagram data via REST or WebSocket |
AppStateService |
Manages local component state and collaborative state |
InfraVisualEffectsService |
Applies visual effects (creation highlights) without affecting history |
User Action Categories:
-
Node Creation: User adds a node via toolbar -->
AppDfdFacade.createNodeWithIntelligentPositioning()-->InfraNodeService.addGraphNode()--> X6 graph adds node --> X6 eventnode:added-->AppDfdFacade.handleNodeAdded()createsCreateNodeOperationfor history. -
Node Drag/Move: User drags a node --> X6 handles mouse events and updates position --> On drag completion,
AppDfdFacade.handleDragCompletion()createsUpdateNodeOperationwith position change for history. -
Node Deletion: User selects a node and presses Delete -->
AppEventHandlersService.onDeleteSelected()--> Starts atomic operation via broadcaster -->InfraX6SelectionAdapter.deleteSelected()--> CreatesDeleteNodeOperation/DeleteEdgeOperationfor each cell. -
Edge Creation: User drags from a source port --> X6 validates the connection -->
AppEdgeService.handleEdgeAdded()validates DFD rules --> CreatesCreateEdgeOperationfor history.
Collaborative vs Solo Mode Decision:
Change Event
|
v
DfdCollaborationService.isCollaborating()?
|
+-- Yes --> Has edit permissions?
| |
| +-- Yes --> InfraWebsocketCollaborationAdapter.sendDiagramOperation()
| | --> WebSocket broadcast to collaborators
| |
| +-- No --> Block change, show error
|
+-- No --> Solo mode: record in AppHistoryService
--> Trigger auto-save via AppDiagramService.saveDiagramChanges()
History Filtering Logic:
The system filters changes to determine what to record in history:
- Position and size changes: Included in history as semantic changes.
-
Visual attributes only (filters, selection styling): Excluded from history via
AppOperationStateManager.executeVisualEffect(). - Tool changes: Filtered out during history processing.
-
Remote changes during sync: Excluded via the
isApplyingRemoteChangeflag.
Performance Considerations:
-
Batching: Multiple related changes are batched into single operations via
AppOperationStateManager.executeAtomicOperation(). - Filtering: Visual-only changes are filtered to reduce noise.
-
Debouncing: Auto-save is debounced via
AppHistoryServiceto prevent excessive API calls. - History Limits: History stacks are capped to prevent memory issues.
For implementation details, see the source repository: docs/migrated/reference/architecture/dfd-change-propagation/user-actions-flow.md
Auto-save Decision Trees (pages/dfd/application/services/):
The auto-save system uses comprehensive decision logic documented in Mermaid flowcharts:
- Primary Auto-save Decision Flow: Determines when changes should trigger persistence based on load state, collaboration mode, and semantic vs. visual changes.
- History Recording Decision Tree: Controls which operations are recorded in undo/redo history.
- Visual Attribute Detection: Identifies and filters visual-only changes (selection effects, shadows, port highlights).
- Collaboration Mode History Handling: Manages the relationship between local and server-side history in collaborative sessions.
- Error Scenarios and Recovery: Documents save failure handling and history corruption recovery flows.
For complete decision tree diagrams, see: docs/migrated/reference/architecture/dfd-change-propagation/autosave-decision-tree.md
Threat Management (pages/tm/components/threat-page/):
- Framework-based threat analysis (STRIDE and others via
threat_typearray) - Threat categorization with CVSS scoring and CWE identifiers
- Mitigation tracking
This section defines the standards for providing services in the tmi-ux Angular application. Following these standards ensures consistent behavior, prevents circular dependencies, and maintains clean architecture.
1. Use Root-Level Provisioning for Shared Services
Services that manage application-wide state or are used across multiple components use root-level provisioning:
@Injectable({
providedIn: 'root',
})
export class AuthService {
// Service implementation
}Examples of root-provided services:
- Authentication services (
AuthService) - API services (
ApiService,ThreatModelService) - Logging services (
LoggerService) - Global state management services
- WebSocket services for real-time collaboration
2. Use Component-Level Provisioning for Component-Specific Services
Services that are tightly coupled to a specific component's lifecycle are provided at the component level:
@Component({
selector: 'app-example',
providers: [ComponentSpecificService],
})
export class ExampleComponent {
// Component implementation
}Examples of component-provided services:
- Graph adapters (e.g., X6 adapters in DFD component)
- Event handlers specific to a component
- History managers for undo/redo within a component
- Services that manage component-specific UI state
1. Duplicate Provisioning
Incorrect -- providing a root-level service again in a component:
// service.ts
@Injectable({
providedIn: 'root' // Already provided at root
})
export class SharedService {}
// component.ts
@Component({
providers: [SharedService] // DON'T DO THIS - creates duplicate instance
})Correct -- use the root-provided instance:
// component.ts
@Component({
// No providers array needed for root-provided services
})
export class ExampleComponent {
constructor(private sharedService: SharedService) {}
}2. Circular Dependencies
Incorrect -- core services depending on feature services:
// In core/services/
export class CoreService {
constructor(private featureService: FeatureService) {} // Upward dependency
}Correct -- use dependency injection tokens or interfaces:
// In core/services/
export const FEATURE_HANDLER = new InjectionToken<FeatureHandler>('FeatureHandler');
export class CoreService {
constructor(@Optional() @Inject(FEATURE_HANDLER) private handler?: FeatureHandler) {}
}| Category | Location | Provisioning | Examples |
|---|---|---|---|
| Core Services | src/app/core/services/ |
providedIn: 'root' |
ApiService, LoggerService, AuthService
|
| Feature Services | src/app/pages/[feature]/services/ |
Context-dependent | Shared: root; Component-specific: component |
| Infrastructure Adapters | src/app/pages/[feature]/infrastructure/adapters/ |
Component providers | X6 graph adapters, DOM manipulation services |
| Shared UI Services | src/app/shared/services/ |
providedIn: 'root' |
NotificationService, DialogService
|
When reviewing or refactoring services:
- Check if the service is already provided at root level
- Remove duplicate provisioning from component providers arrays
- Verify no circular dependencies exist
- Ensure core services don't import from feature modules
- Test that singleton behavior is preserved where expected
- Document any exceptions to these standards with clear justification
- Unit tests should mock dependencies appropriately
- Integration tests should verify singleton behavior
- Use
TestBed.configureTestingModule()to override provisioning in tests when needed
Some valid exceptions to these standards include:
- Testing isolation: Providing a service at the component level for easier testing.
- Multiple instances required: Rare cases where multiple service instances are needed.
- Legacy code: Existing patterns that are too risky to refactor immediately.
Document all exceptions with a comment explaining the reasoning.
The tmi-ux application generates PDF reports on the client side using the pdf-lib library, with diagram images rendered from stored SVG data.
Architecture Overview:
[DFD Editor] ──save──► [SVG Thumbnail Capture] ──► [API Storage]
│ │
▼ ▼
Base64 SVG Data diagram.image.svg
│
▼
[PDF Report Request] ──► [ThreatModelReportService] ──► [SVG→PNG Conversion]
│ │
▼ ▼
[pdf-lib Document] ◄── [Embedded PNG Images]
│
▼
[Downloaded PDF]
Key Components:
-
ThreatModelReportService(pages/tm/services/report/threat-model-report.service.ts): Main service for PDF generation using pdf-lib -
AppExportService(pages/dfd/application/services/app-export.service.ts): Prepares diagram exports and SVG optimization -
AppSvgOptimizationService(pages/dfd/application/services/app-svg-optimization.service.ts): SVGO-based SVG optimization for thumbnails
Diagram Image Storage Model:
Diagrams store pre-rendered SVG images for use in PDF reports and thumbnails.
interface DiagramImage {
svg?: string; // Base64-encoded SVG representation
update_vector?: number; // Version when SVG was generated
}
interface Diagram {
id: string;
name: string;
// ... other fields
image?: DiagramImage; // Pre-rendered image data
}SVG Capture Flow:
- When a user saves a diagram or navigates away from the DFD editor, the component captures an SVG thumbnail.
- The
_captureDiagramSvgThumbnail()method indfd.component.tsuses X6'stoSVG()export. - The SVG is processed and optimized via
AppExportService.processSvg()with SVGO optimization. - The base64-encoded SVG is stored with the diagram via
saveManuallyWithImage().
PDF Rendering Flow:
-
ThreatModelReportService.generateReport()creates a pdf-lib document. - For each diagram with stored SVG data (
diagram.image?.svg):- The SVG is decoded from base64.
-
convertSvgToPng()renders the SVG to a canvas at 300 DPI for print quality. - The PNG data is embedded into the PDF document.
- If no SVG is available, the system shows placeholder text as a fallback.
Print Quality Considerations:
- DPI Scaling: Canvas renders at 4.17x scale (300 DPI / 72 PDF points)
- Aspect Ratio: Preserved from original SVG dimensions
- Width: Uses full printable width minus margins
- Page Size: Configurable (US Letter, A4) via user preferences
Localization Support:
- Custom fonts loaded per language (Noto Sans family for international character support)
- RTL support for Arabic and Hebrew
- Translated section headers and labels via Transloco
ThreatModel (1) ──< (N) Diagram
│ │
│ └──< (N) Cell (stored as JSON array in Diagram)
│
├──< (N) Threat
│
├──< (N) Asset
│
├──< (N) Document
│
├──< (N) Note
│
└──< (N) Repository
Metadata (polymorphic, keyed by EntityType + EntityID)
- Applies to: ThreatModel, Diagram, Threat, Asset, Document, Note, Repository
The primary container for security analysis work.
Fields:
-
id(UUID) - Unique identifier -
name(string) - Display name -
description(string) - Detailed description -
owner(User object) - Owner user -
authorization(array) - Access control list -
created_at(timestamp) - Creation time -
modified_at(timestamp) - Last modification -
created_by(User object) - User who created the threat model -
status(string) - Status in the organization's SDLC process -
threat_model_framework(string) - Framework (default: STRIDE) -
alias(string array) - Alternative names/identifiers -
is_confidential(bool) - Immutable after creation -
issue_uri(string) - Link to issue tracker -
security_reviewer(User object, optional) - Assigned security reviewer -
project_id(UUID, optional) - Associated project -
diagram_count(int) - Number of diagrams (calculated) -
threat_count(int) - Number of threats (calculated) -
document_count(int) - Number of documents (calculated) -
asset_count(int) - Number of assets (calculated) -
note_count(int) - Number of notes (calculated)
Authorization Structure:
{
"owner": { "email": "alice@example.com", "name": "Alice" },
"authorization": [
{
"provider_id": "bob@example.com",
"principal_type": "user",
"provider": "google",
"role": "writer"
},
{
"provider_id": "security-team",
"principal_type": "group",
"provider": "google",
"role": "reader"
},
{
"provider_id": "everyone",
"principal_type": "group",
"provider": "*",
"role": "reader"
}
]
}A visual representation of system data flows.
Fields:
-
id(UUID) - Unique identifier -
threat_model_id(UUID) - Parent threat model -
name(string) - Diagram name -
description(string) - Purpose/scope -
type(string) - Diagram type with version (e.g., "DFD-1.0.0") -
cells(array) - Diagram elements (nodes and edges following X6 structure) -
update_vector(int) - Server-managed monotonic version counter for conflict resolution -
image(object, optional) - Pre-rendered SVG image data with version -
color_palette(array, optional) - Custom color palette for diagram elements -
include_in_report(bool) - Whether to include in generated reports -
timmy_enabled(bool) - Whether AI assistant is enabled
Node Shape Types:
-
actor- External entity -
process- Processing component -
store- Data storage -
security-boundary- Trust boundary -
text-box- Annotations
Edge Shape Types:
-
flow- Data flow between nodes
Cell Structure (follows AntV/X6 format):
{
"id": "uuid",
"shape": "process",
"data": {
"label": "Authentication Service",
"_metadata": [{"key": "...", "value": "..."}]
}
}Node cells include position and size in their X6 data properties. Edge cells include source and target references.
An identified security risk or vulnerability.
Fields:
-
id(UUID) -
threat_model_id(UUID) -
name(string) - Threat name -
description(string) - Detailed description -
threat_type(string array) - Threat classification types (e.g., STRIDE categories like "Spoofing", "Tampering") -
severity(string) - Severity level -
priority(string) - Priority level for addressing -
status(string) - Current status -
mitigation(string) - Recommended or planned mitigation -
mitigated(bool) - Whether the threat has been mitigated -
score(float) - Numeric risk/impact score -
cvss(array) - CVSS scoring information -
cwe_id(string array) - CWE identifiers -
diagram_id(UUID, optional) - Associated diagram -
cell_id(UUID, optional) - Associated cell within diagram -
asset_id(UUID, optional) - Associated asset -
issue_uri(string, optional) - Link to issue tracker
┌─── Application Layer ───┐
│ Go Structs & Interfaces │
├─── Business Logic ──────┤
│ Domain Models & Rules │
├─── Data Access Layer ───┤
│ Repository Pattern │
├─── Storage Layer ───────┤
│ RDBMS │ Redis │
│ (Persistent)│ (Temporary) │
└─────────────────────────┘
TMI supports five relational databases via GORM: PostgreSQL, MySQL, SQLite, SQL Server, and Oracle.
- ACID Compliance: Strong consistency for business data.
- Relational Integrity: Foreign key constraints.
- Schema Migrations: GORM AutoMigrate for all supported databases.
- Performance: Optimized indexes.
- Session Storage: JWT session validation.
- Real-time Coordination: WebSocket connection tracking.
- Caching: Frequently accessed data.
- Rate Limiting: Request throttling.
For a comprehensive guide to how authentication works in TMI -- including detailed sequence diagrams, client credentials, and helper functions -- see Authentication.
The TMI system implements authentication flows across both components:
- OAuth 2.0 with PKCE (RFC 7636) for enhanced security
- Client Credentials Grant for automation and service-to-service authentication
- SAML 2.0 for enterprise SSO integration
- JWT tokens for API authentication (HS256, RS256, or ES256 signing)
For detailed sequence diagrams and visual documentation of all authentication flows, see the source repository at docs/migrated/reference/architecture/oauth-flow-diagrams.md, which includes:
- Complete PKCE flow sequences with Redis state management.
- Token refresh flows and lifecycle diagrams.
- State management and CSRF protection details.
- Multi-provider support (Google, GitHub, Microsoft, TMI test provider).
- SAML SP-initiated and IdP-initiated flows.
- Error handling scenarios.
[Client] → [OAuth Provider] → [TMI Auth] → [JWT Token] → [Protected Resources]
↑ ↑ ↑ ↑ ↑
Login Provider Auth Token Exchange Bearer Auth Resource Access
-
Client Initiation: Client redirects to TMI OAuth endpoint
GET /oauth2/authorize?idp=google -
Provider Selection: User selects OAuth provider
-
Provider Authentication: User authenticates with provider
-
Authorization Grant: Provider returns authorization to TMI
-
Token Exchange: TMI exchanges authorization for user info and generates JWT
-
Client Access: Client receives JWT token for API access
{ "access_token": "eyJhbGc...", "token_type": "Bearer", "expires_in": 3600 }
The tmi backend uses a consolidated user identification architecture with clear separation between internal and external identifiers.
| Field | Purpose | Visibility |
|---|---|---|
internal_uuid |
Database primary key, foreign keys, rate limiting, quotas | Internal only (never in API responses) |
provider |
OAuth provider name (tmi, google, github, microsoft, azure) | Internal and API |
provider_user_id |
Provider's user identifier (in JWT sub claim) |
In API id field |
email |
User's email address | API responses |
name |
Display name | API responses |
-
Provider-Scoped Users: Users from different OAuth providers are treated as separate users, even when they share the same email address. For example,
alice@googleandalice@githubare distinct users. -
Consolidated Schema: All user data is stored in a single
userstable with(provider, provider_user_id)as a unique constraint. There is no separateuser_providerstable. -
JWT Claims: The JWT
subclaim always contains theprovider_user_id, never theinternal_uuid. This ensures that external systems work with provider identifiers. -
Context Utilities: Handler code uses
GetUserFromContext(),GetUserInternalUUID(), andGetUserProviderID()for type-safe access to user identity. -
Redis Caching: User lookups are cached with a 15-minute TTL to reduce database queries.
CREATE TABLE users (
internal_uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider TEXT NOT NULL,
provider_user_id TEXT NOT NULL,
email TEXT NOT NULL,
name TEXT NOT NULL,
-- ... other fields
UNIQUE(provider, provider_user_id)
);The tmi backend provides comprehensive user and group management through admin APIs and provider-scoped endpoints.
TMI uses hard deletion with automatic ownership transfer:
- Begin a transaction.
- For each owned threat model:
- Find an alternate owner (another user with the
ownerrole). - If an alternate owner exists, transfer ownership and remove the deleting user's permissions.
- If no alternate owner exists, delete the threat model (CASCADE deletes diagrams, threats, etc.).
- Find an alternate owner (another user with the
- Delete remaining permissions (reader/writer on other threat models).
- Delete the user record (CASCADE deletes related entities).
- Commit the transaction.
- Write an audit log entry with deletion statistics.
This approach ensures:
- Data consistency with the existing self-deletion flow.
- Transactional integrity (no orphaned resources).
- Automatic ownership transfer that preserves collaborative work.
- Hard deletion that complies with data minimization principles.
Groups in TMI follow a provider-scoped model:
- Provider-specific groups: Sourced from IdP claims (SAML/OAuth).
-
Provider-independent groups: Created by administrators with
provider="*". - Special "everyone" group: Grants access to all authenticated users.
TMI uses layered authorization middleware:
-
Admin-Only (
AdministratorMiddleware): All/admin/*endpoints require administrator privileges. -
Same-Provider (
SameProviderMiddleware): SAML/OAuth endpoints validate that the JWTidpclaim matches the path parameter. -
SAML-Only (
SAMLProviderOnlyMiddleware): Rejects non-SAML providers.
| Endpoint | Method | Purpose |
|---|---|---|
/admin/users |
GET | List all users with filtering |
/admin/users/{id} |
GET/PATCH | Get/update user details |
/admin/users |
DELETE | Delete user by provider + provider_id |
/admin/groups |
GET/POST | List/create groups |
/admin/groups/{id} |
GET/PATCH | Get/update group details |
| Endpoint | Purpose |
|---|---|
/saml/providers/{idp}/users |
List users from caller's SAML provider |
/oauth2/providers/{idp}/groups |
List groups from caller's provider |
Security: Users can only access data from their own provider (prevents cross-provider information leakage).
The tmi-ux application implements client-side session management that automatically logs users out when authentication tokens expire, with advance warning and the option to extend the session.
AuthService (src/app/auth/services/auth.service.ts):
- Handles token storage, validation, and refresh.
- Key methods:
getValidToken(),refreshToken(),isTokenValid(),shouldRefreshToken()(15-minute window),logout().
SessionManagerService (src/app/auth/services/session-manager.service.ts):
- Manages the session lifecycle with timers.
- Monitors token expiry times and sets appropriate timers.
- Shows a warning dialog 5 minutes before token expiry for inactive users.
- Coordinates with AuthService for token refresh.
JWT Interceptor (src/app/auth/interceptors/jwt.interceptor.ts):
- Attaches tokens automatically and handles reactive refresh.
- Adds Bearer tokens to API requests.
- Handles 401 responses with automatic token refresh and request retry.
SessionExpiryDialogComponent (src/app/core/components/session-expiry-dialog/):
- Shows a countdown timer 5 minutes before session expiry.
- Provides "Extend Session" and "Logout" buttons.
- Automatically dismisses when the user takes action or the timer expires.
| Type | Token Structure | Expiry | Refresh Mechanism |
|---|---|---|---|
| OAuth (Google, GitHub, etc.) | JWT access + refresh token | 1 hour (access), longer (refresh) |
/oauth2/refresh endpoint |
| Local Development | Fake JWT token | Configurable via authTokenExpiryMinutes
|
N/A (self-contained) |
| Test User | Mock JWT token | Same as local | Creates new mock token |
- Login: User authenticates --> AuthService stores token --> SessionManager starts expiry timers.
- Active Session: Activity check timer runs every minute; proactive refresh occurs 15 minutes before expiry for active users.
- Warning: 5 minutes before expiry (for inactive users), SessionExpiryDialog shows a countdown.
- Extension: User clicks "Extend Session" --> token is refreshed --> timers reset.
- Logout: Manual, automatic (expired), or failed refresh --> all timers stop --> redirect to home.
- warningTime: 5 minutes before expiry to show warning for inactive users
- proactiveRefreshTime: 15 minutes before expiry for proactive refresh of active users
- activityCheckInterval: 1 minute for checking user activity
Proactive Refresh (for active users):
- Activity check timer detects active user with token expiring within 15 minutes
- Automatic background refresh without user interaction
Warning Dialog Refresh (for inactive users):
- Warning dialog appears 5 minutes before expiry
- User clicks "Extend Session" to trigger refresh
Reactive Refresh (401 handling):
- JWT Interceptor detects 401 response
- Attempts token refresh and retries original request
- Token Encryption: Tokens are encrypted in localStorage using a browser fingerprint.
- Session Fixation Prevention: OAuth flows include CSRF protection via state parameters.
- Token Rotation: Refresh tokens are rotated on each refresh.
- Cross-tab Behavior: All tabs share the same token storage; logging out in one tab affects all tabs.
For complete session management details including sequence diagrams, see the source repository: docs/migrated/reference/architecture/session-management.md
Owner:
- Full control over resource
- Can read, write, and delete
- Can change owner and authorization
- Can delete the resource
Writer:
- Can read and modify resource
- Cannot change owner or authorization fields
- Cannot delete the resource
Reader:
- Read-only access
- Cannot modify anything or delete the resource
-
Authorized User Create: Any authenticated user may create a new empty object.
-
Owner Role Permissions: Users with the owner role can read any field and write to any mutable field in the object, and may delete the object.
-
Writer Role Permissions: Users with the writer role can read any field and write to any mutable field EXCEPT the owner and authorization fields. Writers may not delete the object.
-
Reader Role Permissions: Users with the reader role can only read fields but cannot modify anything or delete the object.
-
Owner Field Precedence: The user listed in the "owner" field automatically gets owner role permissions, regardless of what appears in the authorization field. If the same user appears in both the owner field and authorization list, the owner field takes absolute precedence and they receive owner permissions.
-
Owner Transfer Protection: When an owner changes the owner field to a different user, the handler automatically adds the original owner as a principal in the authorization field with "owner" role. This prevents owners from losing access when transferring ownership.
-
Principal Duplication Validation: Input validation prevents duplicate principals in the authorization field to maintain data integrity. Requests containing duplicate principals will be rejected as invalid.
-
Permission Resolution for Multiple Roles: If a user appears multiple times in the authorization field with different roles, the system grants the highest role (owner > writer > reader). However, input validation should prevent this scenario.
-
Role Updates: If a patch request for the authorization field includes a principal that already exists in the authorization field, the new role value will overwrite the existing role for that principal.
-
Role Precedence: Owner > Writer > Reader.
-
Canonical Order: There is no canonical ordering to entries in authorization; authorization checks do not short-circuit on the first match for a user. If the requesting user is not the owner, then all authorization entries are considered in the authorization decision.
-
Pseudo-Group "everyone": The special group "everyone" grants access to all authenticated users regardless of their identity provider (IdP) or actual group memberships. When an authorization entry has
principal_type: "group"andprovider_id: "everyone", any authenticated user receives the specified role. The provider field is ignored for the "everyone" pseudo-group.
The system uses the following logic to determine a user's effective permissions:
Owner Check First: If the user matches the value in the "owner" field, they receive owner-level permissions regardless of any authorization list entries.
Authorization List Check: If the user is not the owner, the system checks the authorization list for their permissions. All entries in authorization are checked in accordance with rule 11.
Multiple Role Resolution: If a user appears multiple times in the authorization list with different roles (which should be prevented by input validation), the system grants the highest role.
Owner-Principal Duplication: The system gracefully handles cases where the owner also appears in the authorization list:
- Owner in auth list with lower role: Owner field wins
- Owner in auth list with equal role: Both provide same permissions
- Supports ownership transitions: Allows scenarios for safe ownership transfers with no unowned intermediate state
-
Authorization checking is primarily performed by middleware. The middleware allows creates for authorized users (rule 1), and for requests involving a specific object, the middleware retrieves the Owner and Authorization fields from the server, and implements rules 2, 3, 4, 5, 8 and 11.
-
Rules 6, 7, and 9 are implemented in the handler, since they require reading the entire request.
The "everyone" pseudo-group is a cross-IdP group that matches all authenticated users:
-
Provider ID:
"everyone" -
Principal Type:
"group" -
Provider Requirement: None (the provider field is ignored for pseudo-groups; conventionally set to
"*") -
Behavior: Any authenticated user automatically matches this group, regardless of:
- Their identity provider (e.g., tmi, Google, SAML)
- Their actual group memberships from the IdP
- Their email domain or other user attributes
Public Read Access:
{
"owner": {"email": "admin@example.com"},
"authorization": [
{
"provider_id": "everyone",
"principal_type": "group",
"provider": "*",
"role": "reader"
}
]
}Public Write Access:
{
"owner": {"email": "admin@example.com"},
"authorization": [
{
"provider_id": "everyone",
"principal_type": "group",
"provider": "*",
"role": "writer"
}
]
}Mixed Authorization (everyone + specific grants):
{
"owner": {"email": "admin@example.com"},
"authorization": [
{
"provider_id": "everyone",
"principal_type": "group",
"provider": "*",
"role": "reader"
},
{
"provider_id": "editors-team",
"principal_type": "group",
"provider": "google",
"role": "writer"
},
{
"provider_id": "reviewer@example.com",
"principal_type": "user",
"provider": "google",
"role": "writer"
}
]
}In this example:
- All authenticated users can read (via "everyone")
- Members of the "editors-team" group from Google can write
- The user "reviewer@example.com" can write
- The owner "admin@example.com" has full control
Role Precedence with Pseudo-Groups:
When a user matches multiple authorization entries (including "everyone"), the highest role wins:
-
alice@example.comwith explicit owner role receives owner permissions - All other authenticated users receive reader permissions (via "everyone")
Team Collaboration:
{
"owner": {"email": "alice@example.com"},
"authorization": [
{
"provider_id": "bob@example.com",
"principal_type": "user",
"provider": "google",
"role": "owner"
},
{
"provider_id": "charlie@example.com",
"principal_type": "user",
"provider": "google",
"role": "writer"
}
]
}Certain fields are server-controlled and cannot be modified by users:
- ID (UUID) - Generated by the server at object creation time and never changed thereafter
- created_at - Set by the server to the current system time at object creation, represented in UTC in RFC 3339 format
- modified_at - Set by the server to the current system time at object mutation, represented in UTC in RFC 3339 format
- created_by - Set at creation (from JWT)
- Count fields - Calculated from database (diagram_count, threat_count, document_count, asset_count, note_count)
Attempts by users to change created_at or modified_at values are rejected.
The tmi-ux Angular application enforces role-based access control in the UI through a reactive authorization service and component-level permission checks.
ThreatModelAuthorizationService (pages/tm/services/threat-model-authorization.service.ts):
A centralized service that manages threat model authorization state reactively.
// Observable streams for reactive permission checking
get canEdit$(): Observable<boolean> // Writer or Owner
get canManagePermissions$(): Observable<boolean> // Owner only
get currentUserPermission$(): Observable<'reader' | 'writer' | 'owner' | null>
// Synchronous methods for imperative checks
canEdit(): boolean
canManagePermissions(): boolean
getCurrentUserPermission(): 'reader' | 'writer' | 'owner' | nullPermission Checking Patterns:
Components subscribe to the authorization service and update local properties.
// In component initialization
this.authorizationService.canEdit$.subscribe(canEdit => {
this.canEdit = canEdit;
this.updateFormEditability(); // Enable/disable form controls
});Template-Level Enforcement:
UI elements are conditionally rendered or disabled based on permissions.
<!-- Hide action buttons for readers -->
@if (canEdit) {
<button (click)="addAsset()">Add Asset</button>
}
<!-- Show visual indicator for read-only mode -->
@if (!canEdit && !isNewThreatModel) {
<mat-icon [matTooltip]="'common.readOnlyTooltip' | transloco">edit_off</mat-icon>
}Dialog Read-Only Mode:
All editor dialogs receive an isReadOnly flag to disable editing when the user has reader permissions.
const dialogData: AssetEditorDialogData = {
asset: existingAsset,
isReadOnly: !this.canEdit, // Passed to dialog
};Method Guards:
Component methods that modify data include early-return guards.
addAsset(): void {
if (!this.canEdit) {
this.logger.warn('Cannot add asset - insufficient permissions');
return;
}
// ... proceed with add operation
}This client-side enforcement complements the server-side RBAC to provide defense in depth and a better user experience by preventing unauthorized actions before API calls.
[User A] ──WebSocket──┐
├── [TMI WebSocket Hub] ──Redis── [Session State]
[User B] ──WebSocket──┘ │
├── [Operation Log] ──RDBMS
└── [Conflict Resolution]
The server (tmi) implements the WebSocket protocol, and the client (tmi-ux) consumes it.
Connection URL:
ws://localhost:8080/threat_models/{tm_id}/diagrams/{diagram_id}/ws
Authenticated WebSocket connections:
- The WebSocket endpoint is behind JWT authentication middleware (Bearer token in the HTTP header).
- A ticket-based authentication flow is also available: clients first request a short-lived ticket via
GET /ws/ticket?session_id={id}, then present the ticket when connecting. - Tickets expire after 30 seconds and are single-use.
- User permissions are validated on connection (read access to the threat model is required).
- The user is added to the collaboration session upon connection.
Diagram Operations:
{
"message_type": "diagram_operation",
"user_id": "alice@example.com",
"operation_id": "uuid",
"operation": {
"type": "patch",
"cells": [
{
"id": "cell-uuid",
"operation": "add",
"data": { "shape": "process", "x": 100, "y": 200 }
}
]
}
}Presenter Mode:
{
"message_type": "presenter_request",
"user_id": "alice@example.com"
}Cursor Sharing:
{
"message_type": "presenter_cursor",
"user_id": "alice@example.com",
"cursor_position": { "x": 150, "y": 300 }
}- Multi-user Editing: Simultaneous diagram editing with conflict resolution.
- Presenter Mode: A designated presenter with cursor sharing for meetings.
- State Synchronization: Automatic state correction and resync when conflicts are detected.
- Undo/Redo: Collaborative undo/redo with history management.
Update Vector: Each diagram has an update_vector (version number).
State Sync Message: On connection or resync, the client receives the current state:
{
"message_type": "diagram_state",
"diagram_id": "uuid",
"update_vector": 42,
"cells": [ /* current state */ ]
}Conflict Detection: The server detects conflicts when the client's version does not match.
State Correction: The server sends a correction message to resync the client:
{
"message_type": "state_correction",
"update_vector": 45
}The following matrix shows which systems each operation type affects in the tmi-ux DFD component.
| Operation Type | X6 Graph | History | Auto-save | WebSocket | Visual Effects | State Store | Collaboration |
|---|---|---|---|---|---|---|---|
| User Node Creation | Direct | Semantic | Triggered | If collab | Highlight | Update | Broadcast |
| User Node Drag | Direct | Position | Triggered | If collab | None | Update | Broadcast |
| User Node Delete | Direct | Semantic | Triggered | If collab | None | Update | Broadcast |
| User Edge Creation | Direct | Semantic | Triggered | If collab | Highlight | Update | Broadcast |
| User Selection | Selection | Excluded | No save | No broadcast | Selection | Update | If presenter |
| User Hover | Visual | Excluded | No save | No broadcast | Hover | No update | No broadcast |
| Remote Node Add | Applied | Suppressed | No save | Received | Highlight | Update | Source |
| Remote Node Update | Applied | Suppressed | No save | Received | None | Update | Source |
| Remote Node Delete | Applied | Suppressed | No save | Received | None | Update | Source |
| Diagram Load | Batch | Suppressed | No save | No broadcast | None | Full update | No broadcast |
| Undo/Redo (Solo) | History | Applied | Triggered | No broadcast | None | Update | Not collab |
| Undo/Redo (Collab) | Applied | Suppressed | No save | Server-side | None | Update | Server managed |
| Port Visibility | Visual | Excluded | No save | No broadcast | Ports | No update | No broadcast |
| Cell Properties | Direct | Metadata | Triggered | If collab | None | Update | Broadcast |
| Threat Changes | No change | No history | Triggered | No broadcast | None | No update | No broadcast |
Legend:
- Affected: The system is involved in processing this operation
- Not Affected: The system does not process this operation
- If collab: Only affected when in collaborative mode
- If presenter: Only affected for presenter in collaborative mode
1. Collaborative Mode Detection
Multiple services check DfdCollaborationService.isCollaborating(). This is the most critical decision point, affecting almost every operation flow.
When collaborating:
- Use WebSocket for persistence.
- Suppress local history.
- Enable operation broadcasting via
AppEventHandlersService. - Check collaboration permissions.
When in solo mode:
- No permission checks.
- Use REST for persistence.
- Enable local history.
- Disable the broadcaster.
2. History Filtering Decision
AppOperationStateManager.shouldExcludeAttribute() determines which operations create history entries and trigger auto-save. Visual-only attributes (filters, ports, highlights, tool changes) are excluded from history to prevent cluttering undo/redo.
3. Permission Validation
InfraWebsocketCollaborationAdapter.sendDiagramOperation() checks permissions before sending operations. In collaborative mode, it checks DfdCollaborationService.hasPermission('edit'). If collaboration permissions have not yet loaded, it falls back to the threat model permission.
4. Auto-save Validation
The DFD component's auto-save logic validates that:
- The initial diagram load has completed.
- The user is not in collaborative mode (WebSocket handles persistence in that case).
- All required data is available.
5. Visual Effect Application
InfraVisualEffectsService.applyCreationHighlight() prevents visual conflicts by:
- Skipping cells with active effects.
- Skipping currently selected cells.
- Using history suppression during animations.
WebSocket Failure Recovery
InfraWebsocketCollaborationAdapter._sendOperationWithRetry() handles failures:
- Network/Timeout errors: Queue for retry with exponential backoff.
- Permission errors: Block the operation immediately.
- Connection lost: Queue operations for reconnection.
State Correction
When the server detects a version mismatch (the client's update vector differs from the server's), it sends a state correction message that triggers a full diagram resync.
For complete implementation details, see the source repository: docs/reference/architecture/dfd-change-propagation/
- Encryption in Transit: TLS 1.3 for all communications.
- Encryption at Rest: Database and file storage encryption.
- Data Validation: Input sanitization and validation, including JSON schema validation, SQL injection prevention, and XSS prevention in the web application.
- Audit Logging: A comprehensive audit trail for all operations, recording who accessed what, when modifications occurred, and what changes were made.
JWT Security:
- Configurable signing algorithm: HS256, RS256, or ES256.
- Configurable expiration (default: 1 hour / 3600 seconds).
- Key management via environment variables (HMAC secret, RSA key paths, or ECDSA key paths).
OAuth Security:
- CSRF protection with the state parameter.
- Authorization code flow (not implicit flow).
- Token validation on every request.
Session Management:
- Redis-based session storage.
- Automatic cleanup of expired sessions.
- Logout invalidates tokens.
- Middleware-based Authorization: Every request is checked before reaching the handler.
- Resource-level Permissions: Permissions are checked for specific resources, not just endpoints.
- Principle of Least Privilege: The default role is reader; higher permissions require explicit grants.
- Chainguard Base Images: The tmi server uses Chainguard images for a minimal attack surface with daily security updates.
-
Non-root Execution: All containers run as
nonroot:nonrootby default. - Security Scanning: Regular vulnerability scanning with Grype (Anchore).
-
Static Binaries: Built with
CGO_ENABLED=0for no runtime dependencies. - Minimal Attack Surface: No shell, package manager, or unnecessary tools in runtime images (~57 MB total).
- Stateless Servers: The tmi servers are designed for horizontal scaling.
- Load Balancing: Round-robin or least-connections load balancing.
- Session Affinity: Redis-based session storage for multi-server deployment.
- Database Scaling: Read replicas and connection pooling.
- Caching Strategy: Multi-layer caching with Redis and application cache.
- Connection Pooling: Database and Redis connection pooling.
- Lazy Loading: On-demand resource loading and pagination.
- CDN Integration: Static asset delivery through CDN.
- Memory Management: Efficient Go garbage collection tuning.
- Connection Limits: Appropriate limits for concurrent connections.
- Rate Limiting: API and WebSocket rate limiting.
- Resource Quotas: Per-user and per-organization resource limits.
Docker Images: Multi-stage builds with Chainguard base images.
- Builder:
cgr.dev/chainguard/go:latestfor secure Go compilation. - Runtime:
cgr.dev/chainguard/static:latestfor minimal attack surface (~57 MB).
Static Binaries: Built with CGO_ENABLED=0 for maximum portability.
Security Hardening: Non-root execution (nonroot:nonroot), no shell in runtime.
Configuration Management: Environment-based configuration.
Health Checks: Comprehensive health check endpoints.
Database Support: Container builds support PostgreSQL, MySQL, SQL Server, and SQLite. Oracle is excluded due to a CGO requirement.
- Kubernetes Deployment: Cloud-native orchestration option.
- Service Mesh: Advanced networking and security.
- GitOps: Infrastructure and application deployment automation.
- Blue-Green Deployment: Zero-downtime deployment strategy.
TMI provides admin-level user and group management APIs.
Admin-Only APIs (Cross-Provider) --- all implemented:
-
GET/PATCH/DELETE /admin/users- List, update, and delete users across all providers. -
GET/POST/PATCH/DELETE /admin/groups- Manage groups, including provider-independent groups (provider="*"). - Group deletion currently returns 501 (placeholder for future implementation).
SAML UI-Driven APIs (Provider-Scoped) --- all implemented:
-
GET /saml/providers/{idp}/users- List users for SAML provider autocomplete. -
GET /oauth2/providers/{idp}/groups- List groups with authorization checks.
Implementation Components:
- Database stores:
api/user_store.go,api/group_store.go - Admin handlers:
api/admin_user_handlers.go,api/admin_group_handlers.go - SAML handlers:
api/saml_user_handlers.go - Provider auth middleware:
api/provider_auth_middleware.go
For complete design decisions, see the source repository: docs/migrated/developer/planning/user-group-management-decisions.md
The TMI source repository contains reference documentation organized in the docs/reference/ directory.
| Directory | Content |
|---|---|
docs/reference/architecture/ |
System architecture diagrams, OAuth flow diagrams, ADRs |
docs/reference/features/ |
Feature implementation documentation |
docs/reference/libraries/ |
Third-party library integration guides (see DFD-Graphing-Library-Reference) |
docs/reference/schemas/ |
Database schema definitions and data models |
docs/reference/apis/ |
OpenAPI, AsyncAPI, and Arazzo specifications |
The docs/reference/features/ directory contains technical documentation for major features.
-
collaborative-editing.md- Real-time collaborative editing implementation, covering WebSocket communication architecture, state synchronization, conflict resolution, presence and awareness, and permission management (Owner/Writer/Reader roles). -
dfd-user-interaction-guide.md- DFD editor user interaction patterns, covering node and edge manipulation, selection and multi-select, keyboard shortcuts, context menus, and visual feedback. -
status-severity-priority-values.md- Status, severity, and priority value definitions.
-
collaboration-protocol.md- Complete WebSocket collaboration protocol specification, including roles (Host, Participant, Presenter), message types, session lifecycle, undo/redo mechanics, and security considerations. -
tmi-openapi.json- OpenAPI 3.0.3 specification for the REST API. -
tmi-asyncapi.yml- AsyncAPI specification for WebSocket real-time collaboration. -
tmi.arazzo.yaml/tmi.arazzo.json- Arazzo workflow specifications with PKCE OAuth flows.
For detailed API specifications, see API-Specifications.
The tmi-ux ThreatModel validation system provides client-side validation for threat model objects, ensuring they conform to the OpenAPI specification and maintain internal consistency. The system is flexible and extensible, supporting different diagram types and validation requirements.
import { ThreatModelValidatorService } from './validation';
@Component({...})
export class ThreatModelComponent {
constructor(private validator: ThreatModelValidatorService) {}
}// Full validation (schema + diagrams + references + custom rules)
const result = this.validator.validate(threatModel);
if (result.valid) {
console.log('Threat model is valid!');
} else {
console.log('Validation errors:', result.errors);
console.log('Warnings:', result.warnings);
}
// Schema-only validation (faster, useful during editing)
const schemaResult = this.validator.validateSchema(threatModel);
// Reference-only validation (useful for incremental validation)
const refResult = this.validator.validateReferences(threatModel);import { ValidationConfig, FieldValidationRule } from './validation';
const customRules: FieldValidationRule[] = [
{
field: 'name',
required: true,
type: 'string',
minLength: 5,
maxLength: 100,
pattern: /^[A-Za-z0-9\s-]+$/,
},
{
field: 'custom_field',
required: false,
customValidator: (value, context) => {
if (value && !value.startsWith('PREFIX_')) {
return ValidationUtils.createError(
'INVALID_PREFIX',
'Custom field must start with PREFIX_',
context.currentPath + '.custom_field',
);
}
return null;
},
},
];
const config: Partial<ValidationConfig> = {
includeWarnings: true, // Include warnings in result (default: true)
failFast: false, // Stop on first error (default: false)
maxErrors: 50, // Maximum errors before stopping (default: 100)
customRules, // Additional field validation rules
};
const result = this.validator.validate(threatModel, config);You can extend validation for new diagram types by implementing the DiagramValidator interface:
import { DiagramValidator, ValidationContext, ValidationError } from './validation';
class CustomDiagramValidator implements DiagramValidator {
diagramType = 'CUSTOM-2.0.0';
versionPattern = /^CUSTOM-2\.\d+\.\d+$/;
validate(diagram: any, context: ValidationContext): ValidationError[] {
const errors: ValidationError[] = [];
if (!diagram.customProperty) {
errors.push(
ValidationUtils.createError(
'MISSING_CUSTOM_PROPERTY',
'Custom diagrams must have customProperty',
ValidationUtils.buildPath(context.currentPath, 'customProperty'),
),
);
}
return errors;
}
validateCells(cells: any[], context: ValidationContext): ValidationError[] {
// Custom cell validation logic
return [];
}
}
// Register the custom validator
this.validator.registerDiagramValidator(new CustomDiagramValidator());The validation result provides detailed information about validation status.
interface ValidationResult {
valid: boolean; // Overall validation status
errors: ValidationError[]; // Blocking errors
warnings: ValidationError[]; // Non-blocking warnings
metadata: {
timestamp: string; // ISO 8601 timestamp
validatorVersion: string; // Validator version (1.0.0)
duration: number; // Validation time in milliseconds
};
}
interface ValidationError {
code: string; // Error code for programmatic handling
message: string; // Human-readable message
path: string; // JSONPath to the field
severity: 'error' | 'warning' | 'info';
context?: Record<string, unknown>; // Additional context data
}| Code | Description |
|---|---|
FIELD_REQUIRED |
Required field is missing |
INVALID_TYPE |
Field type doesn't match expected type |
INVALID_ENUM_VALUE |
Value is not in allowed enum values |
MIN_LENGTH_VIOLATION |
String/array is too short |
MAX_LENGTH_VIOLATION |
String/array is too long |
PATTERN_MISMATCH |
String doesn't match required pattern |
| Code | Description |
|---|---|
INVALID_DIAGRAM_REFERENCE |
Threat references non-existent diagram |
INVALID_CELL_REFERENCE |
Threat references non-existent cell |
INVALID_THREAT_MODEL_REFERENCE |
Threat has wrong threat_model_id |
OWNER_NOT_IN_AUTHORIZATION |
Owner not present in authorization list |
ORPHANED_CELL_REFERENCE |
Cell ID specified without diagram ID |
ORPHANED_THREATS |
Threats not associated with any diagram |
| Code | Description |
|---|---|
UNSUPPORTED_DIAGRAM_TYPE |
No validator found for diagram type |
INVALID_CELL |
Cell object is malformed |
MISSING_CELL_ID |
Cell is missing required ID |
INVALID_CELL_ID |
Cell ID is not a valid UUID |
MISSING_SHAPE |
Cell is missing shape property |
INVALID_CELL_TYPE |
Cell shape is not a valid node or edge type |
DUPLICATE_CELL_IDS |
Multiple cells have same ID |
INVALID_EDGE_SOURCE |
Edge source references non-existent cell |
INVALID_EDGE_TARGET |
Edge target references non-existent cell |
SELF_REFERENCING_EDGE |
Edge source equals target |
MISSING_POSITION |
Node missing position coordinates |
MISSING_SIZE |
Node missing size dimensions |
INVALID_DIMENSIONS |
Node dimensions below minimum requirements |
| Code | Description |
|---|---|
VALIDATION_EXCEPTION |
Internal validation error |
MAX_ERRORS_EXCEEDED |
Too many errors found |
async importThreatModel(file: File) {
try {
const data = JSON.parse(await file.text());
const result = this.validator.validate(data);
if (!result.valid) {
this.showValidationErrors(result.errors);
return;
}
await this.threatModelService.importThreatModel(data);
} catch (error) {
console.error('Import failed:', error);
}
}@Component({...})
export class ThreatModelEditComponent {
private validationSubject = new Subject<any>();
ngOnInit() {
// Debounced validation for performance
this.validationSubject.pipe(
debounceTime(500),
distinctUntilChanged(),
switchMap(threatModel =>
of(this.validator.validateSchema(threatModel))
)
).subscribe(result => {
this.validationErrors = result.errors;
this.validationWarnings = result.warnings;
});
}
onThreatModelChange(threatModel: any) {
this.validationSubject.next(threatModel);
}
}-
Use schema-only validation (
validateSchema) during editing for faster feedback. -
Use full validation (
validate) before saving or importing. - Debounce validation in real-time scenarios to avoid excessive processing.
-
Configure
maxErrorsto limit processing when many errors are expected. -
Use
failFastfor quick feedback when only the first error matters.
- Implement the
DiagramValidatorinterface. - Define validation rules for the new type.
- Register the validator with
registerDiagramValidator(). - The system automatically handles version patterns (e.g.,
DFD-1.0.x).
- Create
FieldValidationRuleobjects. - Include them in the validation config via
customRules. - Use the
customValidatorfunction for complex logic.
- API-Integration - Learn to integrate with TMI APIs
- Testing - Testing strategies and patterns
- Extending-TMI - Addon development and webhooks
- Security-Best-Practices - Security guidelines
- Using TMI for Threat Modeling
- Accessing TMI
- Authentication
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Timmy AI Assistant
- Metadata and Extensions
- Planning Your Deployment
- Terraform Deployment (AWS, OCI, GCP, Azure)
- Deploying TMI Server
- OCI Container Deployment
- Certificate Automation
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Component Integration
- Post-Deployment
- Branding and Customization
- Monitoring and Health
- Cloud Logging
- Configuration Management
- Config Migration Guide
- Database Operations
- Database Security Strategies
- Security Operations
- Performance and Scaling
- Maintenance Tasks
- Getting Started with Development
- Architecture and Design
- API Integration
- Testing
- Contributing
- Extending TMI
- Dependency Upgrade Plans
- DFD Graphing Library Reference
- Migration Instructions