📋 Migration Summary

Source Tables: 16
Target Models: 12
Decisions: 13

Migrating user authentication, permissions, and SSO from DTX 1.0 to DTX 2.0. Adding new security features: MFA (multi-factor authentication), account lockout protection, device trust, and enhanced audit logging. Restructuring from flat customer-based rights to hierarchical account-scoped permissions.

🎯 Key Actions

⚠️ Key Challenges

DTX 1.0 → DTX 2.0 Migration Plan: Users & Authentication

Overview

Migration of user-related data from DTX 1.0 (MySQL/PHP) to DTX 2.0 (Django/PostgreSQL). This is Phase 2 — depends on Area 2 (Customers/Accounts).

DTX 1.0 Source Tables

Table Purpose
user Main user table (user_id, name, email, password, customer_id, alert flags, etc.)
login Login audit log (user_id, HTTP_USER_AGENT, REMOTE_ADDR, create_time)
rights Permission definitions (30+ rights: Administrator, SuperUser, feature-level)
user_rights Many-to-many user ↔ rights
authorization_key SSO/API auth keys (customer_id, guid, user_id)
identity_provider SAML IdP config (issuerID, metadata, SSO_url, x509cert)
user_scope_filter Per-customer access scoping (filter_type, target_table, target_field)
data_entry_user Links users to data entry provider roles
user_recovery_data Backup/recovery snapshots
user_recovery_ids Recovery email backups
customer_access Many-to-many user ↔ customer (multi-account access)

DTX 2.0 Target Models

Model App Purpose
User (AbstractUser) docutrax_users Main user model
LoginAttempt docutrax_users Login audit with failure tracking
AccountLockout docutrax_users Account lockout tracking (new in 2.0)
UserMFASettings docutrax_users MFA toggle per user (new in 2.0)
OTPToken docutrax_users Temporary OTP for MFA (new in 2.0)
TrustedDevice docutrax_users 7-day device trust (new in 2.0)
UserGridView docutrax_users Saved grid view configs (new in 2.0)
SSOLoginAttempt docutrax_users SSO audit log (new in 2.0)
AccountUserAssignment docutrax_hierarchy User ↔ Account assignment with role
DocutraxTeamAssignment docutrax_hierarchy Staff ↔ Account assignment with role
Permission docutrax_utils Resource + action permission definitions
UserPermission docutrax_utils User ↔ Permission scoped to account/node

Decision #1: Password Migration

Approach: Custom Django hasher (no MySQL install required)

DTX 1.0 stores passwords using MySQL's PASSWORD() function: *HEX(SHA1(SHA1(plaintext))). These are SHA1-based with no salt — weak by modern standards, but we can support them during transition without installing MySQL.

Implementation

  1. Create a custom Django password hasher in pure Python:
# docutrax_users/hashers.py
import hashlib
from django.contrib.auth.hashers import BasePasswordHasher

class MySQLPasswordHasher(BasePasswordHasher):
    algorithm = "mysql"

    def encode(self, password, salt=None):
        first = hashlib.sha1(password.encode()).digest()
        second = hashlib.sha1(first).hexdigest().upper()
        return f"mysql$*{second}"

    def verify(self, password, encoded):
        _, hash_str = encoded.split("$", 1)
        first = hashlib.sha1(password.encode()).digest()
        second = hashlib.sha1(first).hexdigest().upper()
        return hash_str == f"*{second}"

    def safe_summary(self, encoded):
        return {"algorithm": self.algorithm, "hash": "********************"}
  1. Configure Django settings:
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',  # preferred (new)
    'docutrax_users.hashers.MySQLPasswordHasher',         # legacy (fallback)
]
  1. Import users with their old hashes:
for row in legacy_users:
    user = User(username=row['email'], email=row['email'])
    user.password = f"mysql$*{row['password'].lstrip('*')}"
    user.save()

How it works at login

  1. User enters old password
  2. Django tries PBKDF2 — doesn't match
  3. Django tries MySQL hasher — matches
  4. Django automatically re-hashes with PBKDF2 and saves
  5. Next login uses PBKDF2 directly — old hash is gone

Result: Zero friction for users. No reset emails. No MySQL install. Auto-upgrade to strong hashing. Only users who never log in stay on the old hash.


Decision #2: Name Field Splitting

Approach: Smart split + flag for review (Option C)

DTX 1.0 has a single name field. DTX 2.0 expects first_name, last_name, middle_name, display_name.

Known edge cases in DTX 1.0 data

Pattern Example Count (approx)
Normal name "Jack Macejka" Majority
Role name "admin" Many
Name + suffix "Joey Dienes - DC" Some
Trailing space "Brad Wait " Some
Name + generational "James Ross Jr" Some
NULL NULL Some
System account "admin_1", "admin_1_b" Some
Department "Payables" Few
Initials "RKE" Few
Initial + last "B Simpson" Few

Migration logic

For each legacy user:
  1. Store original `name` → `display_name` (always, as-is after trim)
  2. If name is NULL or empty → leave first_name/last_name null, flag for review
  3. If name matches known system patterns (admin, admin_1, etc.) → flag for review
  4. If name is a single word → put in first_name, flag for review
  5. If name has 2+ words → first word = first_name, rest = last_name
  6. Generate a review report of all flagged records

Flagged records will need manual review for:


Decision #3: customer_id → current_account

Approach: Map primary + use existing team/assignment models

Dependency

This migration must run after the Customer → Account migration, since Account records must exist before users can FK to them. An ID mapping table (old_customer_idnew_account_id) is required.

Mapping

DTX 1.0 DTX 2.0 Notes
user.customer_id User.current_account + AccountUserAssignment Primary account
customer_access (multi-account) AccountUserAssignment (additional entries) Multi-account access
Docutrax internal staff DocutraxTeamAssignment Staff with is_docutrax_staff=True

Migration steps

  1. Build ID mapping table from Customer → Account migration
  2. For each 1.0 user:
  3. Set current_account = mapped Account from user.customer_id
  4. Create AccountUserAssignment(account=mapped_account, user=user, role='member')
  5. For each customer_access row:
  6. Create additional AccountUserAssignment entries
  7. For Docutrax internal users (identified by @docutrax.com email or right_id=200):
  8. Set is_docutrax_staff=True
  9. Create DocutraxTeamAssignment entries instead of AccountUserAssignment

Decision #4: Rights → Permissions

Approach: Map core rights, drop client-specific, store originals as reference

DTX 2.0 Permission System

DTX 2.0 uses a resource + action permission model: - Resources: accounts, records, users, coi_evaluations, file_repository, risk_profiles, notifications, audit_logs, permissions, compliance - Actions: read, manage, create - Scoped via UserPermission to account-level or node-level

Mapped Rights (9 rights → direct DTX 2.0 equivalent)

ID DTX 1.0 Right DTX 2.0 Mapping
100 Administrator AccountUserAssignment.role='admin' + all .manage permissions at account-level
200 SuperUser is_docutrax_staff=True + DocutraxTeamAssignment
1 Risk Library risk_profiles.read / risk_profiles.manage
3 Certificate Management coi_evaluations.manage + records.manage
201 File repository file_repository.read / file_repository.manage
206 Non-required document upload file_repository.manage
210 View user rights permissions.read
211 Edit user rights permissions.manage
215 import/export COIs/DOCs records.manage

Unmapped Rights — Feature Gaps (8 rights, decision needed)

ID DTX 1.0 Right Category Decision Needed
2 Claims Management Feature module Does DTX 2.0 have/plan a claims module?
4 Mobile Self-audit Feature module Does DTX 2.0 have/plan mobile self-audit?
202 SO-level Review Granular action Per-record review capability — add as new permission?
207 Set COI and Document send / hold Workflow action Controls send/hold status — add as new permission?
208 Bulk upload Granular action Bulk CSV/file upload — add as new permission?
209 Define outsourced data entry providers Admin action Data entry provider management — add as new permission?
216 SO-level Deactivation Granular action Ability to deactivate records — add as new permission?
1010 On Demand Letters Feature access Letter generation — add as new permission?

Unmapped Rights — UI Toggles (5 rights, recommend DROP)

ID DTX 1.0 Right Reason to Drop
203 Construction filtering on dashboard Client-specific UI toggle — handle via frontend config
204 Insurance program filtering Dashboard filter toggle — handle via frontend config
205 Insurance program display on SO page Page layout toggle — handle via frontend config
212 Direct PH upload link display UI visibility toggle — handle via frontend config
214 View PH expiration hovers Tooltip visibility — handle via frontend config

Unmapped Rights — Client-Specific (8 rights, recommend DROP)

ID DTX 1.0 Right Reason to Drop
1000 Special report for Vornado Per-client hack — not needed in 2.0
1001 File repo / OCR stats viewable Could map to audit_logs.read if needed
1002 Special report for ProSight Per-client hack
1003 Special report for Ferrandino Per-client hack
1004 Special report for ELS Per-client hack
1005 Special report for Toyota Financial Per-client hack
1006 Special filters for Link Per-client hack
1007 Construction SO Data Upload Per-client hack
1008 Edit Management Reports Could map to a new reporting permission if needed
1009 Special filters for Vornado Per-client hack

Remaining rights not categorized above

ID DTX 1.0 Right Notes
213 Acord 25 input form access Specific form access — likely drop or map to coi_evaluations
217 View complete report field definition Report config access — could add as new permission
218 PH history downloads Download capability — could add as new permission
219 White-label Configuration Branding config — could add as new permission

Data Preservation

All original user_rights mappings will be stored in a reference/staging table so nothing is lost, even for dropped rights. This allows re-evaluation later if features are added to DTX 2.0.


Decision #5: Alert Preferences

Approach: Drop user-level flags — DTX 2.0 handles alerts at account/schedule level

DTX 1.0 Alert System (two layers)

Layer 1 — User-level flags (columns on user table): - alert_no_response — notify when vendor doesn't respond - alert_expired — notify on expired certificates - alert_non_compliant — notify on non-compliant vendors - alert_coi — notify on COI events - alert_document — notify on document events

Layer 2 — alert_subscribers (separate table): - Links user_id + customer_id to alert types - Has coi_dow_bitmap (day-of-week bitmask, e.g. 62 = Mon-Fri) - Controls which days alerts are sent

DTX 2.0 Notification System (already built)

DTX 2.0 uses NotificationSchedule at the account level with rich configuration: - Types: coi_request, coi_non_compliance, coi_expiration, coi_review, document_request, document_non_compliance, document_expiration, document_review - Sequences: initial, reminder, final, alert, restart_notices, soon_to_expire, expired - Timing: configurable days before/after events - Plus NotificationQueue for email delivery and InAppNotification for in-app alerts

Mapping

DTX 1.0 DTX 2.0 Equivalent Gap?
user.alert_expired NotificationSchedule (type=coi_expiration) No gap — account-level
user.alert_non_compliant NotificationSchedule (type=coi_non_compliance) No gap
user.alert_coi NotificationSchedule (type=coi_request) No gap
user.alert_document NotificationSchedule (type=document_request) No gap
user.alert_no_response NotificationSchedule (sequence=reminder/final) No gap
alert_subscribers.coi_dow_bitmap No equivalent Small gap — day-of-week send preference

Resolution


Decision #6: Login History

Approach: Don't migrate — start fresh

DTX 1.0's login table contains millions of rows going back to 2013 with basic fields: user_id, HTTP_USER_AGENT, REMOTE_ADDR, create_time.

DTX 2.0's LoginAttempt has a richer schema: username, ip_address, user_agent, successful, failure_reason, timestamp.

Why not migrate

Resolution

Don't migrate. Keep CSV files as compliance archive if needed. DTX 2.0 will start tracking login attempts from day one of go-live.


Decision #7: Identity Providers (SSO)

Approach: Direct migration — parse IdP metadata into DirectorySAMLConfig

Field Mapping

DTX 1.0 (identity_provider) DTX 2.0 (DirectorySAMLConfig) Notes
issuerID idp_entity_id Direct map
SSO_url idp_sso_url Direct map
SLO_url idp_slo_url Direct map
x509cert idp_x509_cert Direct map
NameIDFormat Handled by python3-saml config Not stored as field
metadata (full XML) Not stored — parsed into individual fields Extract fields from XML
SSO_url_binding N/A Default binding used
SLO_url_binding N/A Default binding used

Linking chain

DTX 1.0: identity_provider → authorization_key → customer
DTX 2.0: DirectorySAMLConfig → Directory → root Account

Migration steps

  1. For each identity_provider record:
  2. Look up authorization_key to find the customer_id
  3. Map customer_id → Account (via ID mapping table from Customer migration)
  4. Find the Account's Directory
  5. Create DirectorySAMLConfig with mapped fields
  6. Set idp_type based on issuerID (e.g., contains "okta" → IDP_TYPE_OKTA)
  7. Set is_enabled=True

Scope

Only 1 IdP record exists in the sample data (Okta). This is a small, straightforward migration.

Dependency

Requires Customer → Account → Directory migration to be completed first.


Decision #8: email_ok Flag

Approach: Drop

All sample data shows "no". This was likely an email opt-in/verification flag that was never actively used in DTX 1.0.

DTX 2.0 does not have an equivalent field and does not need one. Email delivery is controlled by notification schedules and account assignments.

Resolution: Drop. No migration needed.


Decision #9: landing_page_view / primary_user

Approach: Drop landing page, map primary_user to role

landing_page_view

Values like "admin/so_summary/dashboard", "coi/client/report" — these are legacy risktoolbox URL paths that have no meaning in DTX 2.0's React frontend with completely different routing.

Resolution: Drop. DTX 2.0 routes are completely different. If per-user default landing pages are needed in the future, they can be added to UserGridView or a new user preference model.

primary_user

Values: "yes" / NULL. Marks the main admin user for a customer account.

Resolution: Already covered. Maps to AccountUserAssignment.role='admin' (Decision #3). Users with primary_user='yes' get the admin role in their account assignment.


Decision #10: Data Entry Providers

Approach: Defer — depends on DTX 2.0 product decision

DTX 1.0 Structure

3 tables supporting outsourced data entry:

Table Fields Purpose
data_entry_provider name, status Provider definitions (UBO, India, Langza Const)
data_entry_user data_entry_provider_id, user_id Links users to providers
data_entry_access data_entry_provider_id, customer_id Which providers access which customers

Only 3 providers exist: UBO, India, Langza Const.

DTX 2.0 Status

No equivalent model exists. If DTX 2.0 uses outsourced data entry providers: - Could map to DocutraxTeamAssignment with a new role (e.g., data_entry) - Or create a dedicated DataEntryProvider model

Resolution

Defer. Store the original data as reference. Decision depends on whether DTX 2.0's operational model includes outsourced data entry teams. If yes, map provider users via DocutraxTeamAssignment with a data_entry role. If no, drop.


Decision #11: Bounced Emails

Approach: Don't migrate — historical data, start fresh

DTX 1.0's bounced_email table tracks email delivery failures from 2018 with: bounce_addr, vendor_id, bounce_type, bounce_subtype, diagnostic, sender_addr, subject.

Why not migrate

Resolution

Don't migrate. Keep CSV as archive. Implement forward-looking bounce tracking via email service webhooks in DTX 2.0.


Decision #12: Site Customization (White-label)

Approach: Partial migration — map to existing Account fields + AccountMetadata

DTX 1.0 Structure

site_customization table with: customer_id, tag_type (IMAGE/TEXT), tag_name, tag_value.

Known tags: - footer_logo (IMAGE) — e.g., "logos/67631/e63.jfif" - header_product_title (TEXT) — e.g., "Risk Management System"

DTX 2.0 Equivalents

DTX 1.0 Tag DTX 2.0 Field Notes
footer_logo Account.logo Direct map — file migration via Area 4's shared S3 utility
header_product_title No model exists yet Gap: AccountMetadata model does not exist in DTX 2.0. Need to either create it or store in Account.comments

Migration steps

  1. For each site_customization record:
  2. Map customer_id → Account (via ID mapping table)
  3. If tag_name='footer_logo': migrate image file to S3, set Account.logo
  4. If tag_name='header_product_title' or other text tags: store in Account.comments with prefix [Site Customization] tag_name: tag_value (or create an AccountMetadata model if DTX 2.0 needs structured customization storage)

Dependency

Requires Customer → Account migration. Logo image files need to be migrated from old file storage to DTX 2.0's S3 bucket.


Decision #13: User Scope Filters

Approach: Store as reference — implement if needed via existing mechanisms

DTX 1.0 Structure

user_scope_filter table with: customer_id, filter_type, filter_str, filter_status, target_table, target_field, target_value.

Example: Users with @programbrokerage.com email automatically scoped to "Program Brokerage Corporation" program.

Filter types: email_domain, wildcard_-_email_dom.

DTX 2.0 Status

DTX 2.0 handles scoping through: - UserPermission — account/node level permission scoping - AccountUserAssignment — user-account relationships - DirectorySAMLConfig.allowed_email_domains — email domain filtering for SSO provisioning

Resolution

Store as reference data. The legacy email-domain auto-scoping is a niche feature. DTX 2.0's explicit permission and assignment model provides more robust scoping. If email-domain-based auto-assignment is needed, it can be implemented via DirectorySAMLConfig.allowed_email_domains or a new filter model.


Unmapped User Fields Summary

DTX 1.0 Field Resolution Notes
open_password Drop All NULL in samples — unused
password_expires_date Drop All NULL — DTX 2.0 doesn't use password expiry
logout_redirect Drop Legacy risktoolbox URLs — irrelevant in 2.0
email_ok Drop All "no" — unused (Decision #8)
edit Covered Maps to permissions (Decision #4)
delete Covered Maps to permissions (Decision #4)
landing_page_view Drop Legacy URL paths (Decision #9)
primary_user Covered Maps to AccountUserAssignment.role='admin' (Decision #9)
login_token Drop Old session token flag — 2.0 uses JWT/session auth
Table Resolution Notes
dashboard_access Covered Same as customer_accessAccountUserAssignment (Decision #3)
data_entry_provider/user/access Deferred Depends on product decision (Decision #10)
alert_subscribers Drop (store as ref) Replaced by NotificationSchedule (Decision #5)
bounced_email Drop Historical data, start fresh (Decision #11)
site_customization Partial migration Logos + titles → Account fields (Decision #12)
user_scope_filter Store as reference Legacy auto-scoping (Decision #13)
user_recovery_data Drop Backup snapshots — not needed in 2.0
user_recovery_ids Drop Recovery email backups — not needed in 2.0

Migration Order (Dependencies)

1. Customers → Accounts       (must be first — users FK to accounts)
2. Users (core fields)         (depends on #1 for current_account FK)
3. Rights → Permissions        (depends on #2 for UserPermission records)
4. customer_access             (depends on #1 and #2 for AccountUserAssignment)
5. Identity Providers → SAML   (depends on Directories existing)
6. Site Customization          (depends on #1 for Account + file storage)
7. Login History               (SKIP — not migrating)
8. Alert Preferences           (SKIP — handled by NotificationSchedule)
9. Bounced Emails              (SKIP — not migrating)
10. Data Entry Providers       (DEFERRED — pending product decision)

Decision Summary

# Decision Status Approach
1 Passwords Resolved Custom MySQL hasher, auto-upgrade on login
2 Name splitting Resolved Smart split + flag for review
3 customer_id → Account Resolved Map primary + AccountUserAssignment
4 Rights → Permissions Resolved Map 9 core, drop 13 legacy, store as reference
5 Alert preferences Resolved Drop — replaced by NotificationSchedule
6 Login history Resolved Don't migrate — start fresh
7 Identity providers Resolved Direct map to DirectorySAMLConfig
8 email_ok flag Resolved Drop — unused
9 landing_page / primary_user Resolved Drop landing page, map primary_user to admin role
10 Data entry providers Deferred Pending product decision
11 Bounced emails Resolved Don't migrate — start fresh
12 Site customization Resolved Partial migration to Account fields + metadata
13 User scope filters Resolved Store as reference data