Class McpServerAudienceService
mcp.server on/off flag that
McpServerToolService.isMcpServerEnabled() reads.
The storage idiom is the one AiSettingService established in the
ai addon — a SettingService entry with a property default —
but deliberately not its field memo. That service's own Javadoc
records why memoising a defaulted read is a trap: a blank read is
indistinguishable from an absent setting, so one failed read pins the
default for the JVM's lifetime. Here the default is the widest audience
there is, so that trap is a permanently open door. A field memo guarded
against only that much still leaves two ways to serve a stale wide
audience, and on a security input staleness is the whole risk:
- a reader that has already read the store when a narrowing save lands publishes the pre-narrowing list after the save dropped the memo, and that value then answers every later call;
ListenerServiceis an in-JVM event bus, so a save on one cluster node never invalidates the memo held by the others, which keep answering the old audience with no TTL to age it out. Reading throughSettingServiceis only better than that where the deployment configures its cache as a clustered or invalidating one — on a local-only cache configuration a per-call read is exactly as stale as a field.
SettingService is itself cache-backed, and it is the very cost
ExoFeatureServiceImpl.isActiveFeature already pays per call for the
global flag on this same path — with the cluster-awareness that a field
above that cache would have removed.
The permission-expression grammar deliberately mirrors
ExoFeatureServiceImpl's own exo.feature.<name>.permissions
fallback. The audience is resolved by this service alone — no
FeaturePlugin is registered for the feature, and nothing on the MCP
request path asks ExoFeatureService.isFeatureActiveForUser, so that
fallback never decides MCP access. Note which of those two facts does the
work: the absence of a plugin is what activates the
property fallback in ExoFeatureServiceImpl, so a caller reaching for
that API would be answered "everybody" wherever the property is unset. The
property is read here instead, as the default of
defaultPermissions, so that a deployment which had narrowed MCP
access with it keeps the exact access it had.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final StringWidest audience of internal users, and the shipped default: it is the value the platform's ownexo.feature.<name>.permissionsexamples use, and it keeps MCP open to every member of/platform/usersas the global flag alone did.static final String -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionbooleanisUserInAudience(String username) Tells whether a user belongs to the MCP audience.voidsavePermissions(List<String> audience) Replaces the audience.
-
Field Details
-
DEFAULT_PERMISSION
Widest audience of internal users, and the shipped default: it is the value the platform's ownexo.feature.<name>.permissionsexamples use, and it keeps MCP open to every member of/platform/usersas the global flag alone did.It is not the exact status quo, and the difference is on purpose. An external user — a login that carries
/platform/externalsand not/platform/users, whichPortalAuthenticationManageradmits — could open an MCP session before this gate existed, because nothing asked a per-user question at all, and cannot after it. Narrowing MCP away from external accounts is the intended reading of "who may use the MCP server"; a deployment that wants them back adds*:/platform/externalsto the audience. This belongs in the addon's upgrade notes, not only here.- See Also:
-
PERMISSIONS_KEY
- See Also:
-
-
Constructor Details
-
McpServerAudienceService
public McpServerAudienceService()
-
-
Method Details
-
isUserInAudience
Tells whether a user belongs to the MCP audience. This is the one place the question is answered: the gate,McpServerToolService.isMcpServerEnabledForUser, asks it directly, so no caller needs to know how the audience is stored.- Parameters:
username- the platform login of the end user, may be null or blank- Returns:
- true when the user matches at least one permission expression of the audience, false when the user is unknown, when no username was resolved, or when an administrator deliberately emptied the audience
-
getPermissions
- Returns:
- the audience as stored, or the property default when nothing is
stored. Read from
SettingServiceon every call and never held in a field — see this class' Javadoc for the two ways a memo here serves a stale, and therefore too wide, audience.
-
savePermissions
Replaces the audience. The change takes effect on the next request, on this node and on every other, because nothing aboveSettingServiceholds the previous value. The broadcast is an extension seam and nothing listens to it today: theaiadministration UI re-reads the audience over REST after each save. An addon that ever keeps a per-user answer of its own would hook here.- Parameters:
audience- the new list of permission expressions; null or empty means nobody, asisUserInAudience(String)documents
-