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.
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).
| 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) |
| 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 |
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.
# 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": "********************"}
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher', # preferred (new)
'docutrax_users.hashers.MySQLPasswordHasher', # legacy (fallback)
]
for row in legacy_users:
user = User(username=row['email'], email=row['email'])
user.password = f"mysql$*{row['password'].lstrip('*')}"
user.save()
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.
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.
| 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 |
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
is_staff=True or is_docutrax_staff=TrueApproach: Map primary + use existing team/assignment models
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_id → new_account_id) is required.
| 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 |
current_account = mapped Account from user.customer_idAccountUserAssignment(account=mapped_account, user=user, role='member')customer_access row:AccountUserAssignment entries@docutrax.com email or right_id=200):is_docutrax_staff=TrueDocutraxTeamAssignment entries instead of AccountUserAssignmentApproach: Map core rights, drop client-specific, store originals as reference
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
| 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 |
| 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? |
| 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 |
| 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 |
| 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 |
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.
Approach: Drop user-level flags — DTX 2.0 handles alerts at account/schedule level
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 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
| 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 |
alert_subscribers → Map user_id + customer_id to the account's notification config. The coi_dow_bitmap (day-of-week) has no equivalent — store as reference data if needed later.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.
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.
Approach: Direct migration — parse IdP metadata into DirectorySAMLConfig
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 |
DTX 1.0: identity_provider → authorization_key → customer
DTX 2.0: DirectorySAMLConfig → Directory → root Account
identity_provider record:authorization_key to find the customer_idcustomer_id → Account (via ID mapping table from Customer migration)DirectorySAMLConfig with mapped fieldsidp_type based on issuerID (e.g., contains "okta" → IDP_TYPE_OKTA)is_enabled=TrueOnly 1 IdP record exists in the sample data (Okta). This is a small, straightforward migration.
Requires Customer → Account → Directory migration to be completed first.
email_ok FlagApproach: 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.
landing_page_view / primary_userApproach: Drop landing page, map primary_user to role
landing_page_viewValues 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_userValues: "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.
Approach: Defer — depends on DTX 2.0 product decision
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.
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
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.
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.
Don't migrate. Keep CSV as archive. Implement forward-looking bounce tracking via email service webhooks in DTX 2.0.
Approach: Partial migration — map to existing Account fields + AccountMetadata
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 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 |
site_customization record:customer_id → Account (via ID mapping table)tag_name='footer_logo': migrate image file to S3, set Account.logotag_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)Requires Customer → Account migration. Logo image files need to be migrated from old file storage to DTX 2.0's S3 bucket.
Approach: Store as reference — implement if needed via existing mechanisms
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 handles scoping through:
- UserPermission — account/node level permission scoping
- AccountUserAssignment — user-account relationships
- DirectorySAMLConfig.allowed_email_domains — email domain filtering for SSO provisioning
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.
| 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_access → AccountUserAssignment (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 |
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 | 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 |