main was 16 days and 259 commits behind the line the product is actually built on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It was neither current nor curated - just stale. This brings it forward and, at the same time, makes it mean something: main is what a receiving site installs, and nothing else. WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides, knowledgebase, machines, network, notifications, printers, slides, tools, warranty - plus applications, which is manifest-less core and always ships. The only manifest dependency inside that set is endpoints on computers, and both are in it, so the closure is self-contained. WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb stay on the site line. So do the Windows installer tree, the VM rehearsal helpers, the agent tooling config, one site's data loader, the internal audit and findings records, and the planner spreadsheet. 1,135 tracked files against 1,438. A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped: deploy/site-profile-universal.json still named all seventeen plugins. It is the file docs/DEPLOY.md tells a site to provision from, so a fresh install would have failed at `flask plugin apply-profile` with PluginError - the first thing a receiving site does, broken by the branch meant to serve them. It now names the twelve that exist, and its comment says the two must agree. agent/ was never refreshed. Whoever populated this branch copied shopdb/, plugins/, frontend/ and tests/ from the site line and left the agent tree at its September state: 22 C# and PowerShell sources missing, including AgentCredentials, EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the site line had deleted still present. The old tree is internally consistent, so it compiles, and nothing in CI builds it - every gate was green over a server that speaks enrollment and per-PC credentials to a bundled agent source that cannot. Resynced whole. DECISIONS THIS ENCODES, rather than defers: - The plugin-authoring lab builds printedparts, whose manifest depends on employees, so it could not stay. The lab remains on the site line and main ships no build-along tutorial. That is a real loss for an adopting developer and is recorded here rather than discovered later. - Recognition, Training and Recertification notifications are no longer seeded. Each splits one notification into a card per employee and renders a photo, which needs a plugin this branch does not bundle. Seeding them would offer an author a type that cannot resolve anybody. A site that installs employees gets them from that plugin's own seed. - Windows/IIS is documented as the legacy path throughout, and the README leads with Docker, because that is what a receiving site gets. The API description is regenerated for THIS branch - 347 paths and 483 operations, down from 415 and 573 - so the published contract describes what is here rather than what the site line serves. docs/CONFIG.md lost the two measuring-tool label keys the code no longer declares, which two lanes had decided opposite ways. Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run rather than skip, and every index line count is derived from the file it names.
163 lines
6.7 KiB
Python
163 lines
6.7 KiB
Python
"""The two endpoints switches are declared, seeded, and have a control.
|
|
|
|
Both were readable by their enforcement code before they were declared:
|
|
|
|
plugin.endpoints.revokeddeniessharedkey agentauth.revoked_denies_shared_key
|
|
plugin.endpoints.bootstrapquerytoken bootstrap._query_token_allowed
|
|
|
|
An undeclared key has no row until somebody writes one, and until it was
|
|
declared the settings API upserted it with valuetype 'string' - so
|
|
bool(Setting.get(key, True)) read the text 'false' as True and the switch did
|
|
nothing. Declaring it fixes both halves: the row is seeded at plugin
|
|
install/enable, and a PUT lands as a real boolean.
|
|
|
|
The UI half is checked by reading the pages. A switch with no control on an
|
|
air-gapped box can only be changed with SQL, which is not a setting.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from plugins.endpoints.api import agentauth, bootstrap
|
|
from plugins.endpoints.plugin import EndpointsPlugin
|
|
from shopdb.core.models import Setting
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
VIEWS = REPO / 'plugins' / 'endpoints' / 'frontend' / 'views'
|
|
|
|
REVOKED_KEY = agentauth.REVOKED_DENIES_SHARED_KEY_SETTING
|
|
QUERY_KEY = bootstrap.QUERY_TOKEN_SETTING
|
|
|
|
|
|
def _declared():
|
|
return {entry['key']: entry for entry in
|
|
EndpointsPlugin().get_settings_defaults()}
|
|
|
|
|
|
@pytest.mark.parametrize('key,value', [(REVOKED_KEY, 'false'),
|
|
(QUERY_KEY, 'true')])
|
|
def test_the_switch_is_declared_with_todays_behaviour(key, value):
|
|
entry = _declared().get(key)
|
|
|
|
assert entry is not None, key + ' is read but never declared'
|
|
assert entry['valuetype'] == 'boolean'
|
|
assert entry['category'] == 'plugin.endpoints'
|
|
# The declared default must be what the code does with no row at all, or
|
|
# seeding a site silently changes its behaviour.
|
|
assert entry['value'] == value
|
|
|
|
|
|
@pytest.mark.parametrize('key', [REVOKED_KEY, QUERY_KEY])
|
|
def test_the_description_names_what_turning_it_on_costs(key):
|
|
"""An operator on the shop floor decides from this text alone."""
|
|
description = _declared()[key]['description']
|
|
|
|
assert len(description) > 120, description
|
|
if key == REVOKED_KEY:
|
|
# The revoke + blockreenroll rollback onto an older agent stops working.
|
|
assert 'rollback' in description
|
|
assert 'enrolls again' in description
|
|
else:
|
|
assert 're-issued' in description or 're-issue' in description
|
|
|
|
|
|
def test_the_declared_defaults_match_what_the_code_reads_with_no_row(db):
|
|
"""No Setting rows at all: the readers fall back to these same values."""
|
|
assert Setting.query.filter_by(key=REVOKED_KEY).first() is None
|
|
assert Setting.query.filter_by(key=QUERY_KEY).first() is None
|
|
|
|
assert agentauth.revoked_denies_shared_key() is False
|
|
assert bootstrap._query_token_allowed() is True
|
|
|
|
|
|
def test_the_upgrade_pass_seeds_them_on_a_site_that_installed_earlier(app, db):
|
|
"""install/enable fire only on a state transition, so a fleet already
|
|
running this plugin gets these rows from `flask plugin upgrade-all`, which
|
|
calls sync_all_plugin_settings -> _seed_plugin_settings.
|
|
|
|
This drives _seed_plugin_settings on a manager built for this test rather
|
|
than the process-wide plugin_manager singleton, and that is deliberate.
|
|
create_app() re-points that singleton, eight test modules build their own
|
|
app, and whatever ran last on this xdist worker owns it: the symptoms are
|
|
"no such table: settings" (seeding against another app's in-memory SQLite)
|
|
and "'NoneType' object has no attribute 'get_all'" (registry left None).
|
|
Both land on whichever test follows, so they move with the split - this one
|
|
passed locally and failed on CI for exactly that reason.
|
|
|
|
init_app is not called here either: it re-registers the plugin blueprints,
|
|
and twice on one app raises "The name 'backups' is already registered".
|
|
Setting _app and _db is all the seeding routine reads.
|
|
"""
|
|
from shopdb.plugins import PluginManager
|
|
from plugins.endpoints.plugin import EndpointsPlugin
|
|
|
|
assert Setting.query.filter_by(key=QUERY_KEY).first() is None
|
|
|
|
manager = PluginManager()
|
|
manager._app = app
|
|
manager._db = db
|
|
assert manager._seed_plugin_settings(EndpointsPlugin()) is True
|
|
|
|
for key, value in ((REVOKED_KEY, False), (QUERY_KEY, True)):
|
|
row = Setting.query.filter_by(key=key).one()
|
|
assert row.valuetype == 'boolean'
|
|
assert row.category == 'plugin.endpoints'
|
|
assert row.get_typed_value() is value
|
|
|
|
|
|
def test_a_put_lands_as_a_boolean_and_flips_the_query_token(client, db,
|
|
auth_headers):
|
|
"""End to end for bootstrapquerytoken through the API the page uses.
|
|
|
|
The declaration is what makes the upsert boolean; as a string row the read
|
|
below would stay True whatever was saved.
|
|
"""
|
|
saved = client.put('/api/settings/' + QUERY_KEY, json={'value': False},
|
|
headers=auth_headers)
|
|
|
|
assert saved.status_code == 200, saved.get_json()
|
|
body = saved.get_json()['data']
|
|
assert body['value'] is False
|
|
assert body['valuetype'] == 'boolean'
|
|
assert body['category'] == 'plugin.endpoints'
|
|
assert bootstrap._query_token_allowed() is False
|
|
|
|
back = client.put('/api/settings/' + QUERY_KEY, json={'value': True},
|
|
headers=auth_headers)
|
|
assert back.get_json()['data']['value'] is True
|
|
assert bootstrap._query_token_allowed() is True
|
|
|
|
|
|
def test_the_settings_api_lists_the_declared_switch(client, db, auth_headers):
|
|
"""The agent-key page reads GET /api/settings/<key> to render the box."""
|
|
client.put('/api/settings/' + QUERY_KEY, json={'value': True},
|
|
headers=auth_headers)
|
|
|
|
resp = client.get('/api/settings/' + QUERY_KEY, headers=auth_headers)
|
|
|
|
assert resp.status_code == 200, resp.get_json()
|
|
assert resp.get_json()['data']['value'] is True
|
|
|
|
|
|
def test_the_enrollment_page_carries_the_eviction_checkbox():
|
|
page = (VIEWS / 'EnrollmentSettings.vue').read_text(encoding='utf-8')
|
|
|
|
assert 'toggle-revokeddenies' in page, 'no checkbox for the eviction switch'
|
|
assert 'onRevokedDeniesToggle' in page
|
|
# It sits with the two switches it belongs beside.
|
|
assert 'toggle-sharedkey' in page and 'toggle-autoenroll' in page
|
|
# The operator has to be told what it breaks before clicking it.
|
|
assert 'rollback' in page
|
|
assert 'enrolls again' in page
|
|
|
|
|
|
def test_the_agent_key_page_carries_the_query_token_control():
|
|
page = (VIEWS / 'AgentKeySettings.vue').read_text(encoding='utf-8')
|
|
|
|
assert QUERY_KEY in page, 'the page does not touch the setting'
|
|
assert 'toggle-querytoken' in page
|
|
assert 'onQueryTokenToggle' in page
|
|
# Turning it off breaks commands already handed out, so say so.
|
|
assert 're-issue' in page
|