Initial commit: add compliance_checks table, per-check metadata on assets, and compliance audit trail
This commit is contained in:
96
app/models/user.py
Normal file
96
app/models/user.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from datetime import datetime
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class User(db.Model):
|
||||
"""
|
||||
Tracked employees / users of IT assets.
|
||||
|
||||
Privacy / GDPR masking:
|
||||
When a user leaves the company, an admin can mask the record.
|
||||
All PII fields are cleared and replaced with a reference to the
|
||||
permanent windows_id so asset history is preserved without
|
||||
exposing personal data during audits.
|
||||
"""
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Permanent, non-PII identifier — used as the anchor for history after masking
|
||||
windows_id = db.Column(db.String(50), unique=True, nullable=False, index=True)
|
||||
|
||||
# PII fields — nulled out when masked
|
||||
first_name = db.Column(db.String(100), nullable=True)
|
||||
last_name = db.Column(db.String(100), nullable=True)
|
||||
email = db.Column(db.String(200), nullable=True)
|
||||
phone = db.Column(db.String(50), nullable=True)
|
||||
|
||||
# Non-PII organisational data — retained after masking
|
||||
department = db.Column(db.String(100), nullable=True)
|
||||
job_title = db.Column(db.String(100), nullable=True)
|
||||
location = db.Column(db.String(100), nullable=True)
|
||||
manager_windows_id = db.Column(db.String(50), nullable=True)
|
||||
|
||||
# Status
|
||||
is_active = db.Column(db.Boolean, default=True) # employed / active in company
|
||||
is_masked = db.Column(db.Boolean, default=False) # PII erased
|
||||
|
||||
masked_at = db.Column(db.DateTime, nullable=True)
|
||||
masked_by_id = db.Column(db.Integer, db.ForeignKey('admin_users.id'), nullable=True)
|
||||
|
||||
# Import metadata
|
||||
import_source = db.Column(db.String(20), default='manual') # manual | ldap | csv
|
||||
ldap_dn = db.Column(db.String(500), nullable=True) # AD Distinguished Name
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
assignments = db.relationship(
|
||||
'Assignment', foreign_keys='Assignment.user_id',
|
||||
backref='user', lazy='dynamic', cascade='all, delete-orphan'
|
||||
)
|
||||
paperwork_docs = db.relationship(
|
||||
'Paperwork', foreign_keys='Paperwork.user_id',
|
||||
backref='user', lazy='dynamic', cascade='all, delete-orphan'
|
||||
)
|
||||
masked_by = db.relationship('AdminUser', foreign_keys=[masked_by_id])
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Display helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
if self.is_masked:
|
||||
return f'[MASKED – WID: {self.windows_id}]'
|
||||
parts = [self.first_name, self.last_name]
|
||||
full = ' '.join(p for p in parts if p)
|
||||
return full or self.windows_id
|
||||
|
||||
@property
|
||||
def display_email(self):
|
||||
return '[MASKED]' if self.is_masked else (self.email or '—')
|
||||
|
||||
@property
|
||||
def display_phone(self):
|
||||
return '[MASKED]' if self.is_masked else (self.phone or '—')
|
||||
|
||||
@property
|
||||
def current_assets(self):
|
||||
"""Returns active assignments."""
|
||||
return self.assignments.filter_by(is_active=True).all()
|
||||
|
||||
def mask(self, admin_user_id):
|
||||
"""Erase PII while preserving the record for asset-history purposes."""
|
||||
self.first_name = None
|
||||
self.last_name = None
|
||||
self.email = None
|
||||
self.phone = None
|
||||
self.is_active = False
|
||||
self.is_masked = True
|
||||
self.masked_at = datetime.utcnow()
|
||||
self.masked_by_id = admin_user_id
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User wid={self.windows_id} masked={self.is_masked}>'
|
||||
Reference in New Issue
Block a user