Add application-driven policy generation with an acknowledgement gate

Ticking "Required on all PCs" now generates a configuration policy that
installs the application on every non-server endpoint it matches, rather
than only flagging a software-compliance report row.

Because that save is a fleet-wide deployment, it requires explicit
acknowledgement. The gate is enforced in the API, not only in the form:
a confirmation dialog that can be sidestepped by POSTing directly is
decoration, not a safeguard. It fires only on the transition INTO the
required state, since re-saving an already-required application deploys
nothing new.

Generated policies carry a new 'autoapp' source. They stay editable, and
hand-editing one transfers ownership to 'local' so it survives a later
uncheck; 'shipped' remains read-only. Existing required applications are
not migrated retroactively - policies are only created on save.

Also adds the monitorable fields that the shipped-policy breakup will
build on, and repairs a mangled non-ASCII byte in copilot-instructions.md
that was failing the style gate.
This commit is contained in:
benjamin.baker
2026-09-07 13:27:59 -04:00
parent f73df42110
commit aa9dafc5ca
10 changed files with 497 additions and 7 deletions

View File

@@ -19,8 +19,10 @@ from shopdb.api import (db, AuditLog, success_response, error_response,
from .routes import computers_bp
from ..models import (ConfigurationPolicy, ConfigurationPolicyAction,
BusinessPolicy, BusinessPolicyItem, POLICY_SOURCE_LOCAL,
POLICY_SOURCE_AUTOAPP,
ACTION_MODE_DESIRED, ACTION_MODE_GATE)
from ..policyflag import policy_flag_enabled
from ..services.apppolicy import disown_if_generated
from ..services.policyscope import validate_scope, ScopeError
_MODES = {ACTION_MODE_DESIRED, ACTION_MODE_GATE}
@@ -258,7 +260,10 @@ def update_configuration_policy(policy_id: int):
if not policy:
return error_response(ErrorCodes.NOT_FOUND,
'Configuration policy not found', http_code=404)
if policy.source != POLICY_SOURCE_LOCAL:
# An autoapp row is generated from an Application's "Required on all PCs"
# checkbox and IS editable: editing it is precisely how an admin takes
# ownership. Genuinely external sources (shipped/managed) stay read-only.
if policy.source not in (POLICY_SOURCE_LOCAL, POLICY_SOURCE_AUTOAPP):
return error_response(ErrorCodes.CONFLICT,
'Policy is managed externally and is read-only',
http_code=409)
@@ -301,6 +306,10 @@ def update_configuration_policy(policy_id: int):
if err:
return err
# Hand-editing a generated policy transfers ownership: from here on the
# application checkbox will not delete it, it will warn instead.
disown_if_generated(policy)
AuditLog.log('updated', 'ConfigurationPolicy', entityname=policy.name)
db.session.commit()
return success_response(_configpolicy_dict(policy),
@@ -317,7 +326,9 @@ def delete_configuration_policy(policy_id: int):
if not policy:
return error_response(ErrorCodes.NOT_FOUND,
'Configuration policy not found', http_code=404)
if policy.source != POLICY_SOURCE_LOCAL:
# As with update: a generated row is the site's to remove, an externally
# managed one is not.
if policy.source not in (POLICY_SOURCE_LOCAL, POLICY_SOURCE_AUTOAPP):
return error_response(ErrorCodes.CONFLICT,
'Policy is managed externally and is read-only',
http_code=409)