Skip to content

fix: enforce the UCP permission matrix - #313

Open
Johannes Berthel (joberthel) wants to merge 1 commit into
fix/admin-ucp-file-persistencefrom
fix/205-ucp-admin-acl
Open

Johannes Berthel (joberthel) wants to merge 1 commit into
fix/admin-ucp-file-persistencefrom
fix/205-ucp-admin-acl

Conversation

@joberthel

@joberthel Johannes Berthel (joberthel) commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor
pr313-ucp-permissions.webm

UCP Administration permissions do not consistently match the actions users need to perform. In particular, users with ucp.editor cannot save UCP settings unless they also have sales_channel.editor.

To reproduce on a Shopware installation with the plugin active:

  1. Create a user with ucp.editor and sales_channel.viewer, but without sales_channel.editor.
  2. Open a UCP-enabled Storefront or Headless sales channel.
  3. Change a UCP setting and click Save.
  4. Open Agentic Commerce Insights as a user without ucp.viewer, including by navigating directly to the Insights route.

Previously, the UCP editor could not save without sales-channel edit access, and Insights access was not consistently restricted by ucp.viewer.

With this change, UCP editors can save UCP settings without sales_channel.editor. Viewers and key rotators see the configuration in read-only mode, and Agentic Commerce Insights require ucp.viewer, including on direct navigation. Product-export Insights keep their existing core permissions.

Closes #205.

  • Tests cover the change, or it needs none, such as documentation or CI configuration.
  • A change merchants notice has an entry under # next version in both changelogs.
  • The change works on Shopware 6.5, 6.6 and 6.7 from one codebase.

@joberthel Johannes Berthel (joberthel) linked an issue Oct 23, 2026 that may be closed by this pull request
5 tasks
@joberthel Johannes Berthel (joberthel) changed the title Fix/205 ucp admin acl fix: enforce the UCP permission matrix Oct 23, 2026
@joberthel
Johannes Berthel (joberthel) marked this pull request as ready for review October 26, 2026 15:43
@joberthel
Johannes Berthel (joberthel) requested a review from a team as a code owner October 26, 2026 15:43
@joberthel
Johannes Berthel (joberthel) marked this pull request as draft October 29, 2026 13:18
@joberthel
Johannes Berthel (joberthel) changed the base branch from main to fix/admin-ucp-file-persistence October 29, 2026 15:37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: The German changelog should use core's name for the tab.

The German Administration calls the Insights tab „Statistiken“, as in sw-sales-channel.detail.productExport.tabInsights. „Agentic-Commerce-Insights“ doesn't appear anywhere in the UI.

}

if (this.isUcpConfigRoute && !this.acl.can('sales_channel.editor')) {
if (!this.acl.can('ucp.editor') || !this.ucpState.loaded) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Save does nothing when the UCP config failed to load.

ucpState.loaded only turns true once getConfig has answered. After a failed request it stays false. allowSaving still enables Save for a ucp.editor on this tab. A click then returns here without a request, a notification or a spinner.

Could you keep Save disabled until ucpState.loaded is true on this route?


const salesChannelId = this.salesChannel?.id;
const form = ucpNormalizeConfig(this.ucpState.form);
const hasUcpChanges = JSON.stringify(form) !== JSON.stringify(ucpNormalizeConfig(this.ucpState.savedForm));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (non-blocking): This adds a third copy of the UCP dirty check to onSave.

The same comparison is at lines 514 and 556. I suggest a hasUcpChanges(form) method that all three branches call.

{% block sw_sales_channel_detail_content_tab_product_export_insights %}{% endblock %}
{% block sw_sales_channel_detail_content_tab_agentic_commerce_integration %}{% endblock %}

{% block sw_sales_channel_detail_content_view %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): This block copies core's whole content view to add one condition.

Core's sw_sales_channel_detail_content_view already passes these props and listeners. Core also has its own branch for the legacy router on 6.5. With the copy, Agentic Commerce channels miss every prop or listener that core adds to this block later.

{% parent %} gives the same result without the copy:

{% block sw_sales_channel_detail_content_view %}
<template v-if="!isAgenticCommerce || shouldRenderAgenticCommerceContent">
    {% parent %}
</template>
<div
    v-else
    class="sw-sales-channel-detail__content-denied"
    …
</div>
{% endblock %}

supportsVueRouterViewSlot and the two new imports can then go.


created() {
this.fetchData();
if (this.canViewInsights) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): The new ucp.viewer checks in this component can't be reached.

The route already requires ucp.viewer. Core's router guard redirects to the privilege error page before the component is created. Could you drop the checks in created(), in fetchData() and on the root element?

},
};

registerOrOverride('sw-sales-channel-detail-product-export-insights', productExportInsightsOverride);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Below 6.7.10, this registers a broken component under core's name.

Core ships sw-sales-channel-detail-product-export-insights from 6.7.10.0 on. Before that, registerOrOverride() registers this config on its own. The result has no template. Its fetchData() calls a $super that doesn't exist.

Where core ships the component, the override never runs for an Agentic Commerce channel without ucp.viewer. shouldRenderAgenticCommerceContent shows the denied banner in its place. Please remove this file and its spec.

});

await goToAdminRoute(withoutUcpViewer, insightsRoute);
await expect(insightsCards(withoutUcpViewer)).toHaveCount(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: The direct navigation only checks that nothing is there.

toHaveCount(0) and the request filter both pass as soon as the skeleton is gone. Cards or requests that come a moment later don't fail the test. Could you first wait for the denied banner or the privilege error page, depending on the route?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ACL matrix for the UCP administration tab

2 participants