Add installer/script delivery metadata to the core software model

AppVersion gains sha256 and silentargs so the endpoint update_app push can pin exact installer bytes and carry per-version silent switches; Application gains installersha256 for the app-level install source. New ShopDB-hosted script library (Script model plus scripts API) backs run_script delivery, with the body fetched by digest on push rather than pre-staged or sent over the command wire. Two idempotent migrations with real downgrades. Script added to the shopdb.api contract surface for plugin use.
This commit is contained in:
benjamin.baker
2026-08-30 14:39:58 -04:00
parent dc2f4266bc
commit 0c2b816a46
12 changed files with 483 additions and 3 deletions

View File

@@ -16,6 +16,8 @@ from .communication import Communication, CommunicationType
from .user import (User, Role, Permission, full_permission_catalog,
guest_grantable_permissions)
from .application import Application, AppVersion
from .script_library import (Script, RUNCONTEXTS, RUNCONTEXT_SYSTEM,
RUNCONTEXT_USER)
from .supportteam import SupportTeam, SupportTeamContact
from .setting import Setting
from .auditlog import AuditLog
@@ -62,6 +64,11 @@ __all__ = [
# Applications
'Application',
'AppVersion',
# Script library (endpoint run_script delivery)
'Script',
'RUNCONTEXTS',
'RUNCONTEXT_SYSTEM',
'RUNCONTEXT_USER',
# Support teams
'SupportTeam',
'SupportTeamContact',

View File

@@ -34,6 +34,11 @@ class Application(BaseModel):
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')
@@ -70,6 +75,12 @@ class AppVersion(db.Model):
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')
@@ -88,7 +99,9 @@ class AppVersion(db.Model):
'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
'isactive': self.isactive,
'sha256': self.sha256,
'silentargs': self.silentargs
}
def __repr__(self):

View File

@@ -0,0 +1,43 @@
"""ShopDB-hosted script library for endpoint run_script delivery.
A script is authored/uploaded once here, and the endpoint agent fetches its
body by digest on push (mirroring the application installer delivery path). The
body never lives on the endpoint ahead of time and never crosses the command
wire; the agent downloads it with its collector credential and verifies sha256.
"""
from shopdb.extensions import db
from .base import BaseModel
# Execution context for a delivered script. SYSTEM is the service account the
# agent already runs as (no user session, cannot see mapped drives); USER runs
# in the interactive logged-on user's session. Default SYSTEM stays fail-safe.
RUNCONTEXT_SYSTEM = 'system'
RUNCONTEXT_USER = 'user'
RUNCONTEXTS = (RUNCONTEXT_SYSTEM, RUNCONTEXT_USER)
class Script(BaseModel):
"""ShopDB-hosted PowerShell script the endpoint agent runs on push."""
__tablename__ = 'scripts'
scriptid = db.Column(db.Integer, primary_key=True)
scriptname = db.Column(db.String(100), unique=True, nullable=False)
scriptdescription = db.Column(db.String(255))
# Stored file name under the scripts upload directory (script-<id>.ps1). The
# body is served by the collector-authenticated download route.
filename = db.Column(db.String(255))
# Digest of the stored body; the agent verifies this before running.
sha256 = db.Column(db.String(64))
# 'system' or 'user' (see RUNCONTEXTS). Per-script so a drive-mapping script
# can target the user session while a machine-wide script runs as SYSTEM.
runcontext = db.Column(db.String(16), default=RUNCONTEXT_SYSTEM, nullable=False)
def to_dict(self):
result = super().to_dict()
# hasbody lets the UI disable pushing a script with no uploaded file.
result['hasbody'] = bool(self.filename and self.sha256)
return result
def __repr__(self):
return f"<Script {self.scriptname}>"