diff --git a/shell/services/AuthServiceStore.js b/shell/services/AuthServiceStore.js index b5c931f1..d3eb5a66 100644 --- a/shell/services/AuthServiceStore.js +++ b/shell/services/AuthServiceStore.js @@ -4,6 +4,7 @@ // a separate empty store rather than a shared path to credential-bearing QML. var services = ({}) +var trustedIds = ({}) function has(id) { return services[String(id || "")] !== undefined @@ -12,11 +13,16 @@ function has(id) { function put(id, service) { var key = String(id || "") if (!key || !service) return + trustedIds[key] = true if (services[key] && services[key] !== service && typeof services[key].destroy === "function") services[key].destroy() services[key] = service } +function isTrusted(id) { + return trustedIds[String(id || "")] === true +} + function ids() { return Object.keys(services) } diff --git a/shell/shell.qml b/shell/shell.qml index d9714a9d..86c0f37e 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -375,7 +375,7 @@ ShellRoot { if (!shell.pluginHasBarCapabilities(manifest)) return false var id = shell.pluginRegistry.resolveEnabledId(String(requestedId || "")) var target = shell.pluginRegistry.installedPlugins[id] - if (!target || shell.isAuthenticationService(target)) return false + if (!target || shell.isAuthenticationService(target, id)) return false if (shell.barEntryConfigured(id)) return true var uiKinds = ["bar-widget", "panel", "overlay", "menu"] for (var i = 0; i < uiKinds.length; i++) @@ -842,9 +842,11 @@ ShellRoot { return serviceFor(shell.pluginRegistry.resolveEnabledId(pluginId)) } - function isAuthenticationService(manifest) { - return !!manifest && Array.isArray(manifest.__hostCapabilities) - && manifest.__hostCapabilities.indexOf("authentication") !== -1 + function isAuthenticationService(manifest, pluginId) { + var key = String(pluginId || (manifest && manifest.id) || "") + return AuthServiceStore.isTrusted(key) + || (!!manifest && Array.isArray(manifest.__hostCapabilities) + && manifest.__hostCapabilities.indexOf("authentication") !== -1) } function ensureService(pluginId) { @@ -857,7 +859,7 @@ ShellRoot { if (!manifest.entryPoints || !manifest.entryPoints.service) return null var url = pluginRegistry.entryPointUrl(manifest, "service") if (!url) return null - var authenticationService = shell.isAuthenticationService(manifest) + var authenticationService = shell.isAuthenticationService(manifest, key) if (authenticationService && AuthServiceStore.has(key)) return null var comp = Qt.createComponent(url, Component.PreferSynchronous) @@ -908,7 +910,7 @@ ShellRoot { if (!Array.isArray(m.kinds) || m.kinds.indexOf("service") === -1) continue if (!m.entryPoints || !m.entryPoints.service) continue if (!pluginRegistry.isEnabled(id)) continue - var authenticationService = shell.isAuthenticationService(m) + var authenticationService = shell.isAuthenticationService(m, id) if (_services[id]) { if (authenticationService) { // A service that gains a trusted authentication capability must move @@ -965,7 +967,7 @@ ShellRoot { && authenticationManifest.entryPoints && authenticationManifest.entryPoints.service if (stillAuthenticationService && pluginRegistry.isEnabled(authenticationId) - && shell.isAuthenticationService(authenticationManifest)) continue + && shell.isAuthenticationService(authenticationManifest, authenticationId)) continue AuthServiceStore.destroy(authenticationId) } } diff --git a/test/shell.d/fixtures/plugin-auth-boundary/AuthStoreOwner.qml b/test/shell.d/fixtures/plugin-auth-boundary/AuthStoreOwner.qml index fa5ae0bc..5ca9c271 100644 --- a/test/shell.d/fixtures/plugin-auth-boundary/AuthStoreOwner.qml +++ b/test/shell.d/fixtures/plugin-auth-boundary/AuthStoreOwner.qml @@ -10,6 +10,10 @@ QtObject { return AuthServiceStore.has(id) } + function isTrusted(id) { + return AuthServiceStore.isTrusted(id) + } + function updateManifest(id, manifest) { AuthServiceStore.updateManifest(id, manifest) } diff --git a/test/shell.d/fixtures/plugin-auth-boundary/shell.qml b/test/shell.d/fixtures/plugin-auth-boundary/shell.qml index a4968468..f133fcfe 100644 --- a/test/shell.d/fixtures/plugin-auth-boundary/shell.qml +++ b/test/shell.d/fixtures/plugin-auth-boundary/shell.qml @@ -75,6 +75,7 @@ ShellRoot { ownSettings: api.updateEntryInline(caller, {}) === true, foreignSettings: api.updateEntryInline("omarchy.lock", {}) === false, authStoreOwnerRetains: authStoreOwner.has("omarchy.lock") === true, + authStoreOwnerRemembersTrust: authStoreOwner.isTrusted("omarchy.lock") === true, authStoreOwnerUpdatesManifest: root.ownService.manifest && root.ownService.manifest.version === "kept", authStoreImportIsolated: authStoreReader.has("omarchy.lock") === false, diff --git a/test/shell.d/plugin-auth-boundary-test.sh b/test/shell.d/plugin-auth-boundary-test.sh index 9859699f..63066355 100755 --- a/test/shell.d/plugin-auth-boundary-test.sh +++ b/test/shell.d/plugin-auth-boundary-test.sh @@ -37,12 +37,32 @@ qml_matches "$shell_qml" 'comp\.createObject\( *manifest\.__isFirstParty *&& *!a fail "third-party and authentication services are detached from the host object tree" qml_matches "$shell_qml" 'AuthServiceStore\.put\( *key, *inst *\)' || fail "authentication services are retained outside the host service map" +qml_matches "$shell_qml" 'AuthServiceStore\.isTrusted\( *key *\)' || + fail "live authentication classification survives public manifest mutation" qml_matches "$shell_qml" 'AuthServiceStore\.updateManifest\( *id, *shell\.publicPluginManifest\( *m *\) *\)' || fail "kept authentication services receive only a public manifest snapshot" qml_matches "$shell_qml" 'if *\( *!serviceKeepLoaded\( *authenticationId *\) *\) *AuthServiceStore\.destroy\( *authenticationId *\)' || fail "keepLoaded authentication services survive plugin rescans" pass "third-party and authentication services are detached from the host object tree" +run_node_test <<'JS' +const fs = require('fs') +const vm = require('vm') +const store = {} +vm.createContext(store) +vm.runInContext( + fs.readFileSync(path.join(root, 'shell/services/AuthServiceStore.js'), 'utf8'), + store +) +const service = { destroy() {} } +store.put('omarchy.lock', service) +store.destroy('omarchy.lock') +assert( + !store.has('omarchy.lock') && store.isTrusted('omarchy.lock'), + 'authentication classification survives service teardown' +) +JS + qml_matches "$shell_qml" 'inst\.shell *= *shell\.pluginShellFor\( *manifest *\)' || fail "service plugins receive a scoped shell facade" qml_matches "$shell_qml" 'item\.shell *= *shell\.pluginShellFor\( *panelEntry\.manifest *\)' ||