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.
291 lines
12 KiB
Vue
291 lines
12 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>Authentication</h2>
|
|
</div>
|
|
|
|
<div class="section-card">
|
|
<div class="setting-group">
|
|
<h3>SAML Single Sign-On</h3>
|
|
<p class="setting-notice">
|
|
<strong>Not implemented yet.</strong> These settings are stored so a
|
|
site can be configured ahead of the feature landing, but nothing
|
|
consumes them today: there is no SAML login route, and no assertion
|
|
is accepted. Sign-in is local accounts only, whatever is set here.
|
|
</p>
|
|
<p class="setting-description">
|
|
When it ships, users will sign in through your organization's identity
|
|
provider (Azure AD, Okta, etc.), with local login available as a
|
|
fallback.
|
|
</p>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Enable SAML SSO</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.saml_enabled }"
|
|
@click="toggleSetting('saml_enabled')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
</div>
|
|
|
|
<template v-if="settings.saml_enabled">
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Identity Provider Metadata URL</span>
|
|
<input
|
|
type="url"
|
|
v-model="settings.saml_idp_metadata_url"
|
|
placeholder="https://login.microsoftonline.com/.../federationmetadata.xml"
|
|
@blur="saveSetting('saml_idp_metadata_url', settings.saml_idp_metadata_url)"
|
|
:disabled="saving"
|
|
>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Service Provider Entity ID</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.saml_entity_id"
|
|
placeholder="https://shopdb.example.com"
|
|
@blur="saveSetting('saml_entity_id', settings.saml_entity_id)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">Unique identifier for this application</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Assertion Consumer Service URL</span>
|
|
<input
|
|
type="url"
|
|
v-model="settings.saml_acs_url"
|
|
placeholder="https://shopdb.example.com/api/auth/saml/acs"
|
|
@blur="saveSetting('saml_acs_url', settings.saml_acs_url)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">URL where SAML responses are sent</small>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label>
|
|
<span>Admin Group Name</span>
|
|
<input
|
|
type="text"
|
|
v-model="settings.saml_admin_group"
|
|
placeholder="ShopDB-Admins"
|
|
@blur="saveSetting('saml_admin_group', settings.saml_admin_group)"
|
|
:disabled="saving"
|
|
>
|
|
<small class="input-hint">SAML group that grants admin role (leave empty to manage manually)</small>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Locked on until SAML exists. A toggle that reads "Allow Local
|
|
Login: off" while password login still works is the dangerous
|
|
one on this page: an administrator can believe they have closed
|
|
the local door, and nothing tells them otherwise. It is not a
|
|
setting anyone can honour yet, so it does not accept a click. -->
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Allow Local Login</span>
|
|
<button
|
|
class="toggle-btn active"
|
|
disabled
|
|
title="Local login cannot be turned off until SAML sign-in exists"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
<small class="input-hint toggle-hint">
|
|
Always on for now. Local accounts are the only way in until SAML
|
|
sign-in ships, so this cannot be turned off yet.
|
|
</small>
|
|
</div>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Auto-Create Users</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: settings.saml_auto_create_users }"
|
|
@click="toggleSetting('saml_auto_create_users')"
|
|
:disabled="saving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
<small class="input-hint toggle-hint">Automatically create user accounts on first SAML login</small>
|
|
</div>
|
|
|
|
<div class="status-indicator">
|
|
<span class="status-dot" :class="samlStatus"></span>
|
|
<span>{{ samlMessage }}</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Collector API credentials. This page is where a site manages who
|
|
may authenticate, and the shared collector key is a credential like
|
|
any other - it just belongs to scripts rather than people. It is
|
|
rendered here because there is no generic key/value settings editor
|
|
in this UI: without a control the seeded row can only be flipped by
|
|
editing the database by hand, which on an air-gapped prod box means
|
|
it is not a setting at all. -->
|
|
<div class="setting-group">
|
|
<h3>Collector API</h3>
|
|
<p class="setting-description">
|
|
The four legacy collector routes (<code>/api/collector/pc</code>,
|
|
<code>/apps</code>, <code>/heartbeat</code>, <code>/bulk</code>) name
|
|
the PC they write in the request body, so the shared collector key is
|
|
an any-host write key on them: whoever holds it can rewrite the
|
|
logged-in user, last boot time, serial number and installed apps of
|
|
any hostname. A collector-scoped managed token is unaffected.
|
|
</p>
|
|
|
|
<div class="setting-row">
|
|
<label class="toggle-label">
|
|
<span>Refuse the shared key on legacy collector routes</span>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: legacySharedKeyRefused }"
|
|
@click="toggleLegacySharedKey"
|
|
:disabled="legacySharedKeySaving"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</label>
|
|
<small class="input-hint toggle-hint">
|
|
Off by default, which is what every site does today. Turning it on
|
|
stops <code>Report-AssetToShopDB.ps1</code> and
|
|
<code>Report-PrintersToShopDB.ps1</code> reporting through those
|
|
routes until they are moved to per-PC credentials, along with
|
|
anything else still calling them with the shared key. Every
|
|
shared-key call is logged as "Legacy collector route used with the
|
|
shared key" whether this is on or off - read those warnings for a
|
|
while first, or turning it on is a guess. On the shipped IIS deploy
|
|
that line names the route but not the caller: the X-Forwarded-For
|
|
rewrite rule in web.config ships disabled, so every request reaches
|
|
the app from 127.0.0.1. Count the routes, not the addresses, unless
|
|
that rule is on. The shipped reporters that post to
|
|
<code>/api/collector/computers</code> and
|
|
<code>/api/collector/printers</code> are not affected. This should
|
|
follow the move to per-PC credentials, not lead it.
|
|
</small>
|
|
</div>
|
|
<div v-if="legacySharedKeyError" class="error-message">
|
|
{{ legacySharedKeyError }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-group">
|
|
<h3>User Management</h3>
|
|
<p class="setting-description">
|
|
Manage local user accounts and roles. Local accounts work alongside SAML when both are enabled.
|
|
</p>
|
|
|
|
<router-link to="/settings/users" class="view-logs-link">
|
|
Manage Users →
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
<div v-if="success" class="settings-success">{{ success }}</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { onMounted, computed, ref } from 'vue'
|
|
import { useSystemSettings } from '../../composables/systemSettings'
|
|
import { settingsApi } from '../../api'
|
|
import { apiError } from '../../utils/apiError'
|
|
|
|
const {
|
|
settings, saving, error, success,
|
|
loadSettings, saveSetting, toggleSetting,
|
|
} = useSystemSettings()
|
|
|
|
// collector.legacysharedkeyrefused is read and written here directly rather
|
|
// than through useSystemSettings: that composable only keeps keys listed in
|
|
// its own reactive map, and this one is core-owned but belongs to no page it
|
|
// already serves. Default false = today's behaviour, so anything but an
|
|
// explicit true renders off.
|
|
const LEGACY_SHARED_KEY_SETTING = 'collector.legacysharedkeyrefused'
|
|
const legacySharedKeyRefused = ref(false)
|
|
const legacySharedKeySaving = ref(false)
|
|
const legacySharedKeyError = ref('')
|
|
|
|
function isTruthySetting(value) {
|
|
// A row seeded boolean comes back as a real true; one created by hand
|
|
// through the settings API before the key was declared can be the string.
|
|
return value === true || value === 'true'
|
|
}
|
|
|
|
async function loadLegacySharedKey() {
|
|
try {
|
|
const { data } = await settingsApi.get(LEGACY_SHARED_KEY_SETTING)
|
|
legacySharedKeyRefused.value = isTruthySetting(data.data?.value)
|
|
} catch (e) {
|
|
// 404 = never seeded on this install. The backend default is off.
|
|
if (e.response?.status !== 404) console.error(e)
|
|
legacySharedKeyRefused.value = false
|
|
}
|
|
}
|
|
|
|
async function toggleLegacySharedKey() {
|
|
const next = !legacySharedKeyRefused.value
|
|
if (next && !window.confirm(
|
|
'Refuse the shared collector key on the legacy routes?\n\n'
|
|
+ 'Report-AssetToShopDB.ps1, Report-PrintersToShopDB.ps1 and anything '
|
|
+ 'else still using the shared key on /pc, /apps, /heartbeat or /bulk '
|
|
+ 'stop reporting until they move to per-PC credentials.')) {
|
|
return
|
|
}
|
|
legacySharedKeySaving.value = true
|
|
legacySharedKeyError.value = ''
|
|
try {
|
|
// PUT upserts, so this also works where the row was never seeded.
|
|
await settingsApi.update(LEGACY_SHARED_KEY_SETTING, next)
|
|
legacySharedKeyRefused.value = next
|
|
} catch (e) {
|
|
legacySharedKeyError.value = apiError(e, 'Failed to save setting')
|
|
console.error(e)
|
|
} finally {
|
|
legacySharedKeySaving.value = false
|
|
}
|
|
}
|
|
|
|
// Per-page connection status (stays with the page, not the composable).
|
|
const samlStatus = computed(() => {
|
|
if (!settings.saml_enabled) return 'inactive'
|
|
// 'pending' rather than a healthy state even when every field is filled in:
|
|
// nothing is connected until the feature exists.
|
|
return 'pending'
|
|
})
|
|
|
|
// Never says "available". It used to report "Configured - SAML login
|
|
// available" as soon as two fields held text, which is a claim about a feature
|
|
// that does not exist: an administrator could reasonably read that as SSO
|
|
// being live and stop issuing local accounts.
|
|
const samlMessage = computed(() => {
|
|
if (!settings.saml_enabled) return 'Off - local accounts only'
|
|
if (!settings.saml_idp_metadata_url) return 'IdP metadata URL not set'
|
|
if (!settings.saml_entity_id) return 'Entity ID not set'
|
|
return 'Settings saved - waiting on SAML support, local accounts still in use'
|
|
})
|
|
|
|
onMounted(() => {
|
|
loadSettings()
|
|
loadLegacySharedKey()
|
|
})
|
|
</script>
|