feat(site): join UI, spoke read-only enforcement, live WAN health, fresh-install guard
Completes the multi-site join layer on top of the v2.2.0 endpoints: - UI (Master Site modal): a fresh install (canJoin) gets a 'Join an Existing Site' form (master URL + stj_ key); a master gets a 'Site Join Keys' manager (mint/revoke/list, key shown once); WAN Sync Health now reflects a live probe. - POST /api/site/ping (Bearer stj_ key, no admin session): lightweight master reachability probe for WAN health (cheap vs /export). - Spoke read-only: directory-write routes (resources/edges/groups/secrets/ grants/driver-action/discovered) reject with 403 pointing at the master. - Fresh-install guard: /api/site/join refuses unless no users beyond the bootstrap admin and no enrolled agents (siteIsFresh), and site-status exposes canJoin so the UI only offers join on a genuinely fresh install. The bootstrap's seeded default resources are NOT the signal (they always exist). - The spoke stores the join key (masterJoinKey) in /config/site.json so WAN health (and a future write-proxy) can reach the master. - Tests: siteIsFresh cases in tests/site_join.test.js.
This commit is contained in:
@@ -101,4 +101,25 @@ function baseDnFrom(conf) {
|
||||
return m ? m[1] : '';
|
||||
}
|
||||
|
||||
module.exports = { scalarResource, scalarEdge, importDirectory, ldapAddArgs, baseDnFrom };
|
||||
// siteIsFresh reports whether this deployment may join a master site
|
||||
// (MULTI_SITE_SPEC.md): no users beyond the bootstrap admin and no enrolled
|
||||
// agents. The bootstrap always seeds a handful of default resources (site →
|
||||
// host → sso/proxy services), so resources are NOT the signal — the operator's
|
||||
// rule is "no users". A directory with real users must never be merged into a
|
||||
// master's; that is the destructive case this guard prevents.
|
||||
async function siteIsFresh({ User, Agent }) {
|
||||
const agents = (Agent && Agent.list ? await Agent.list().catch(() => []) : []);
|
||||
if (agents && agents.length > 0) return false;
|
||||
if (User && typeof User.listDetail === 'function') {
|
||||
try {
|
||||
const users = await User.listDetail();
|
||||
const real = (users || []).filter(u => !u.isServiceAccount);
|
||||
return real.length <= 1; // at most the bootstrap admin
|
||||
} catch (e) {
|
||||
// LDAP unreachable — fall back to the agent-only check.
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = { scalarResource, scalarEdge, importDirectory, ldapAddArgs, baseDnFrom, siteIsFresh };
|
||||
|
||||
Reference in New Issue
Block a user