Ben Baker's installed-software collection, software delivery (run_script and
update_app), agentless adoption tool and command-queue hardening, with his
authorship preserved.
Reviewed before promotion. Fixed: the endpoints plugin tests were never collected
by pytest, the SBOM snapshot table's key would exhaust on MySQL, ingest did not
truncate two columns strict MySQL would reject, and update_app's version branch
pinned a digest for a file that route never serves. Agent packaging moved off
require_role('admin') onto a new endpoints.agentpackage permission.
Contract 0.27.0. Full suite 2231, frontend 314.
109 lines
4.3 KiB
Python
109 lines
4.3 KiB
Python
"""Application tracking models."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from shopdb.extensions import db
|
|
from .base import BaseModel
|
|
|
|
|
|
def _utcnow():
|
|
# naive UTC to match the other DB DateTime columns (stored without tzinfo)
|
|
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
# SupportTeam / SupportTeamContact live in supportteam.py; imported by the
|
|
# models package so the Application.supportteam relationship resolves.
|
|
|
|
|
|
class Application(BaseModel):
|
|
"""Application catalog."""
|
|
__tablename__ = 'applications'
|
|
|
|
appid = db.Column(db.Integer, primary_key=True)
|
|
appname = db.Column(db.String(100), unique=True, nullable=False)
|
|
appdescription = db.Column(db.String(255))
|
|
supportteamid = db.Column(db.Integer, db.ForeignKey('supportteams.supportteamid'))
|
|
isinstallable = db.Column(db.Boolean, default=False)
|
|
applicationnotes = db.Column(db.Text)
|
|
installpath = db.Column(db.String(255))
|
|
applicationlink = db.Column(db.String(512))
|
|
documentationpath = db.Column(db.String(512))
|
|
ishidden = db.Column(db.Boolean, default=False)
|
|
isprinter = db.Column(db.Boolean, default=False)
|
|
islicenced = db.Column(db.Boolean, default=False)
|
|
isrequired = db.Column(
|
|
db.Boolean, default=False,
|
|
comment='Required on all PCs (drives the software-compliance report)'
|
|
)
|
|
image = db.Column(db.String(255))
|
|
# Digest of the app-level uploaded installer (installpath). Lets the
|
|
# endpoint update_app push resolve an install source by app alone, with no
|
|
# AppVersion row: the agent fetches the pinned bytes and verifies this hash.
|
|
# Stamped on package upload; null when installpath is a site share path.
|
|
installersha256 = db.Column(db.String(64))
|
|
|
|
# Relationships
|
|
supportteam = db.relationship('SupportTeam', back_populates='applications')
|
|
versions = db.relationship('AppVersion', back_populates='application', lazy='dynamic')
|
|
|
|
def to_dict(self):
|
|
"""Serialize, flattening the support team + its active contacts.
|
|
|
|
Emits supportteamname, teamurl, and the team's active contacts so the
|
|
frontend needs a single call to render the Support card.
|
|
"""
|
|
result = super().to_dict()
|
|
team = self.supportteam
|
|
result['supportteamname'] = team.teamname if team else None
|
|
result['teamurl'] = team.teamurl if team else None
|
|
result['contacts'] = [
|
|
{'name': c.name, 'sso': c.sso}
|
|
for c in team.active_contacts()
|
|
] if team else []
|
|
return result
|
|
|
|
def __repr__(self):
|
|
return f"<Application {self.appname}>"
|
|
|
|
|
|
class AppVersion(db.Model):
|
|
"""Application version tracking."""
|
|
__tablename__ = 'appversions'
|
|
|
|
appversionid = db.Column(db.Integer, primary_key=True)
|
|
appid = db.Column(db.Integer, db.ForeignKey('applications.appid'), nullable=False)
|
|
version = db.Column(db.String(50), nullable=False)
|
|
releasedate = db.Column(db.Date)
|
|
notes = db.Column(db.String(255))
|
|
dateadded = db.Column(db.DateTime, default=_utcnow)
|
|
isactive = db.Column(db.Boolean, default=True)
|
|
# Delivery metadata for the endpoint installer push. sha256 pins the exact
|
|
# bytes the agent must fetch (warn-not-block for now; hardened later);
|
|
# silentargs carries this version's unattended-install switches because
|
|
# homegrown EXEs vary by developer (MSI is uniform via msiexec).
|
|
sha256 = db.Column(db.String(64))
|
|
silentargs = db.Column(db.String(255))
|
|
|
|
# Relationships
|
|
application = db.relationship('Application', back_populates='versions')
|
|
|
|
# Unique constraint on app + version
|
|
__table_args__ = (
|
|
db.UniqueConstraint('appid', 'version', name='uq_app_version'),
|
|
)
|
|
|
|
def to_dict(self):
|
|
"""Convert to dictionary."""
|
|
return {
|
|
'appversionid': self.appversionid,
|
|
'appid': self.appid,
|
|
'version': self.version,
|
|
'releasedate': self.releasedate.isoformat() if self.releasedate else None,
|
|
'notes': self.notes,
|
|
'dateadded': self.dateadded.isoformat() + 'Z' if self.dateadded else None,
|
|
'isactive': self.isactive,
|
|
'sha256': self.sha256,
|
|
'silentargs': self.silentargs
|
|
}
|
|
|
|
def __repr__(self):
|
|
return f"<AppVersion {self.application.appname if self.application else self.appid} v{self.version}>"
|