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:
110
tests/test_core/test_application_required_policy.py
Normal file
110
tests/test_core/test_application_required_policy.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Tests for the acknowledgement gate on "Required on all PCs".
|
||||
|
||||
Ticking that box is not a reporting flag. It generates a configuration policy
|
||||
that installs the application on every non-server endpoint it matches, so the
|
||||
save is a fleet-wide deployment. The gate exists so nobody triggers one by
|
||||
tabbing through a form.
|
||||
|
||||
The gate lives in the API rather than only in the form on purpose, and that is
|
||||
most of what is pinned here: a confirmation dialog that can be sidestepped by
|
||||
POSTing directly is decoration, not a safeguard. The other half is that it
|
||||
fires only on the transition INTO the required state, since re-saving an app
|
||||
that already has its policy deploys nothing new.
|
||||
"""
|
||||
|
||||
from shopdb.core.models import Application
|
||||
|
||||
|
||||
def _app(db, name='Test App', isrequired=False):
|
||||
app = Application(appname=name, isrequired=isrequired)
|
||||
db.session.add(app)
|
||||
db.session.commit()
|
||||
return app
|
||||
|
||||
|
||||
def test_create_marked_required_is_refused_without_acknowledgement(
|
||||
client, db, auth_headers):
|
||||
response = client.post('/api/applications',
|
||||
json={'appname': 'Fleet App', 'isrequired': True},
|
||||
headers=auth_headers)
|
||||
|
||||
assert response.status_code == 400, response.get_json()
|
||||
message = response.get_json()['data']['error']['message']
|
||||
assert 'install' in message.lower()
|
||||
assert Application.query.filter_by(appname='Fleet App').first() is None
|
||||
|
||||
|
||||
def test_create_marked_required_succeeds_once_acknowledged(
|
||||
client, db, auth_headers):
|
||||
response = client.post('/api/applications',
|
||||
json={'appname': 'Fleet App', 'isrequired': True,
|
||||
'acknowledgepolicy': True},
|
||||
headers=auth_headers)
|
||||
|
||||
assert response.status_code == 201, response.get_json()
|
||||
assert Application.query.filter_by(appname='Fleet App').first().isrequired
|
||||
|
||||
|
||||
def test_update_into_required_is_refused_without_acknowledgement(
|
||||
client, db, auth_headers):
|
||||
app = _app(db)
|
||||
|
||||
response = client.put(f'/api/applications/{app.appid}',
|
||||
json={'isrequired': True}, headers=auth_headers)
|
||||
|
||||
assert response.status_code == 400, response.get_json()
|
||||
# The refusal must leave the flag alone, not half-apply the change.
|
||||
assert db.session.get(Application, app.appid).isrequired is False
|
||||
|
||||
|
||||
def test_update_into_required_succeeds_once_acknowledged(
|
||||
client, db, auth_headers):
|
||||
app = _app(db)
|
||||
|
||||
response = client.put(f'/api/applications/{app.appid}',
|
||||
json={'isrequired': True, 'acknowledgepolicy': True},
|
||||
headers=auth_headers)
|
||||
|
||||
assert response.status_code == 200, response.get_json()
|
||||
assert db.session.get(Application, app.appid).isrequired is True
|
||||
|
||||
|
||||
def test_resaving_an_already_required_app_needs_no_acknowledgement(
|
||||
client, db, auth_headers):
|
||||
# The policy already exists, so this save deploys nothing new. Prompting
|
||||
# again would train people to click through the warning that matters.
|
||||
app = _app(db, isrequired=True)
|
||||
|
||||
response = client.put(f'/api/applications/{app.appid}',
|
||||
json={'isrequired': True,
|
||||
'appdescription': 'Edited'},
|
||||
headers=auth_headers)
|
||||
|
||||
assert response.status_code == 200, response.get_json()
|
||||
|
||||
|
||||
def test_clearing_required_needs_no_acknowledgement(client, db, auth_headers):
|
||||
# Unticking withdraws the policy. It never installs anything, so there is
|
||||
# nothing to warn about.
|
||||
app = _app(db, isrequired=True)
|
||||
|
||||
response = client.put(f'/api/applications/{app.appid}',
|
||||
json={'isrequired': False}, headers=auth_headers)
|
||||
|
||||
assert response.status_code == 200, response.get_json()
|
||||
assert db.session.get(Application, app.appid).isrequired is False
|
||||
|
||||
|
||||
def test_acknowledgement_flag_is_not_persisted_to_the_application(
|
||||
client, db, auth_headers):
|
||||
# It is a control flag for one request, not a column. If it ever reached
|
||||
# the model it would be a stored "yes" that suppresses future warnings.
|
||||
response = client.post('/api/applications',
|
||||
json={'appname': 'Fleet App', 'isrequired': True,
|
||||
'acknowledgepolicy': True},
|
||||
headers=auth_headers)
|
||||
|
||||
assert response.status_code == 201, response.get_json()
|
||||
app = Application.query.filter_by(appname='Fleet App').first()
|
||||
assert not hasattr(app, 'acknowledgepolicy')
|
||||
assert 'acknowledgepolicy' not in response.get_json()['data']
|
||||
Reference in New Issue
Block a user