Skip to content

Commit

Permalink
Mount widget interfaces to admin tool #10701
Browse files Browse the repository at this point in the history
  • Loading branch information
anatol-sialitski committed Sep 25, 2024
1 parent cc487b6 commit c392786
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.enonic.xp.admin.tool;

import java.util.Objects;
import java.util.Set;

import com.google.common.collect.ImmutableSet;

Expand Down Expand Up @@ -30,6 +31,8 @@ public class AdminToolDescriptor

private final ApiMountDescriptors apiMounts;

private final ImmutableSet<String> interfaces;

private AdminToolDescriptor( final Builder builder )
{
key = builder.key;
Expand All @@ -39,6 +42,7 @@ private AdminToolDescriptor( final Builder builder )
descriptionI18nKey = builder.descriptionI18nKey;
allowedPrincipals = PrincipalKeys.from( builder.allowedPrincipals.build() );
apiMounts = Objects.requireNonNullElse( builder.apiMounts, ApiMountDescriptors.empty() );
interfaces = builder.interfaces.build();
}

public DescriptorKey getKey()
Expand Down Expand Up @@ -86,6 +90,16 @@ public boolean isAccessAllowed( final PrincipalKeys principalKeys )
return principalKeys.contains( RoleKeys.ADMIN ) || principalKeys.stream().anyMatch( allowedPrincipals::contains );
}

public Set<String> getInterfaces()
{
return interfaces;
}

public boolean hasInterface( final String interfaceName )
{
return interfaces.contains( interfaceName );
}

public boolean isAppLauncherApplication()
{
return displayName != null;
Expand Down Expand Up @@ -128,6 +142,8 @@ public static final class Builder

private final ImmutableSet.Builder<PrincipalKey> allowedPrincipals = ImmutableSet.builder();

private final ImmutableSet.Builder<String> interfaces = ImmutableSet.builder();

private Builder()
{
}
Expand Down Expand Up @@ -174,6 +190,12 @@ public Builder apiMounts( final ApiMountDescriptors apiMountDescriptors )
return this;
}

public Builder addInterface( final String interfaceName )
{
this.interfaces.add( interfaceName );
return this;
}

public AdminToolDescriptor build()
{
return new AdminToolDescriptor( this );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class WidgetDescriptor

private final ImmutableMap<String, String> config;

private static final String URL_PREFIX = "_/widget/";
private static final String URL_PREFIX = "_/admin/widget/";

private WidgetDescriptor( final Builder builder )
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import com.enonic.xp.admin.tool.AdminToolDescriptor;
import com.enonic.xp.admin.tool.AdminToolDescriptorService;
import com.enonic.xp.admin.widget.WidgetDescriptor;
import com.enonic.xp.admin.widget.WidgetDescriptorService;
import com.enonic.xp.app.ApplicationKey;
Expand All @@ -22,23 +24,31 @@
import com.enonic.xp.web.WebResponse;
import com.enonic.xp.web.universalapi.UniversalApiHandler;

@Component(immediate = true, service = UniversalApiHandler.class, property = {"applicationKey=widget", "apiKey=",
@Component(immediate = true, service = UniversalApiHandler.class, property = {"applicationKey=admin", "apiKey=widget",
"allowedPrincipals=role:system.admin.login", "allowedPrincipals=role:system.admin"})
public class WidgetApiHandler
implements UniversalApiHandler
{
private static final Pattern WIDGET_API_PATTERN = Pattern.compile( "^/(_|api)/widget/(?<appKey>[^/]+)/(?<widgetKey>[^/]+)" );
private static final Pattern WIDGET_API_PATTERN = Pattern.compile( "^/(_|api)/admin/widget/(?<appKey>[^/]+)/(?<widgetKey>[^/]+)" );

private static final Pattern TOOL_PREFIX_PATTERN = Pattern.compile( "^/admin/(?<appKey>[^/]+)/(?<toolName>[^/]+)" );

private static final String GENERIC_WIDGET_INTERFACE = "generic";

private final ControllerScriptFactory controllerScriptFactory;

private final WidgetDescriptorService widgetDescriptorService;

private final AdminToolDescriptorService adminToolDescriptorService;

@Activate
public WidgetApiHandler( @Reference final ControllerScriptFactory controllerScriptFactory,
@Reference final WidgetDescriptorService widgetDescriptorService )
@Reference final WidgetDescriptorService widgetDescriptorService,
@Reference final AdminToolDescriptorService adminToolDescriptorService )
{
this.controllerScriptFactory = controllerScriptFactory;
this.widgetDescriptorService = widgetDescriptorService;
this.adminToolDescriptorService = adminToolDescriptorService;
}

@Override
Expand Down Expand Up @@ -66,6 +76,8 @@ public WebResponse handle( final WebRequest webRequest )
throw WebException.forbidden( String.format( "You don't have permission to access [%s]", descriptorKey ) );
}

verifyMounts( widgetDescriptor, webRequest );

final PortalRequest portalRequest = createPortalRequest( webRequest, descriptorKey );

final ResourceKey script = ResourceKey.from( descriptorKey.getApplicationKey(),
Expand All @@ -74,6 +86,29 @@ public WebResponse handle( final WebRequest webRequest )
return controllerScriptFactory.fromScript( script ).execute( portalRequest );
}

private void verifyMounts( final WidgetDescriptor widgetDescriptor, final WebRequest webRequest )
{
if ( !widgetDescriptor.hasInterface( GENERIC_WIDGET_INTERFACE ) && webRequest.getEndpointPath() != null )
{
final Matcher toolMatcher = TOOL_PREFIX_PATTERN.matcher( webRequest.getRawPath() );
if ( toolMatcher.find() )
{
final DescriptorKey toolDescriptorKey =
DescriptorKey.from( resolveApplicationKey( toolMatcher.group( "appKey" ) ), toolMatcher.group( "toolName" ) );
final AdminToolDescriptor adminToolDescriptor = adminToolDescriptorService.getByKey( toolDescriptorKey );
if ( adminToolDescriptor != null && !adminToolDescriptor.getInterfaces().isEmpty() )
{
if ( widgetDescriptor.getInterfaces().stream().noneMatch( adminToolDescriptor::hasInterface ) )
{
throw WebException.notFound(
String.format( "Widget [%s] is not mounted to admin tool [%s]", widgetDescriptor.getKey(),
toolDescriptorKey ) );
}
}
}
}
}

private PortalRequest createPortalRequest( final WebRequest webRequest, final DescriptorKey descriptorKey )
{
final PortalRequest portalRequest =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public class XmlAdminToolDescriptorParser

private static final String API_DESCRIPTOR_TAG_NAME = "api";

private static final String INTERFACES_DESCRIPTOR_TAG_NAME = "interfaces";

private static final String INTERFACE_DESCRIPTOR_TAG_NAME = "interface";

private static final int APPLICATION_KEY_INDEX = 0;

private static final int API_KEY_INDEX = 1;
Expand Down Expand Up @@ -55,6 +59,16 @@ protected void doParse( final DomElement root )
}

this.builder.apiMounts( ApiMountDescriptors.from( parseApiMounts( root.getChild( APIS_DESCRIPTOR_TAG_NAME ) ) ) );

final DomElement interfaces = root.getChild( INTERFACES_DESCRIPTOR_TAG_NAME );
if ( interfaces != null )
{
final List<DomElement> interfaceList = interfaces.getChildren( INTERFACE_DESCRIPTOR_TAG_NAME );
for ( DomElement anInterface : interfaceList )
{
this.builder.addInterface( anInterface.getValue().trim() );
}
}
}

private List<ApiMountDescriptor> parseApiMounts( final DomElement apisElement )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.enonic.xp.admin.tool.AdminToolDescriptorService;
import com.enonic.xp.admin.widget.WidgetDescriptor;
import com.enonic.xp.admin.widget.WidgetDescriptorService;
import com.enonic.xp.app.ApplicationKey;
Expand Down Expand Up @@ -34,13 +35,16 @@ public class WidgetApiHandlerTest

private WidgetDescriptorService widgetDescriptorService;

private AdminToolDescriptorService adminToolDescriptorService;

@BeforeEach
public void setUp()
{
this.controllerScriptFactory = mock( ControllerScriptFactory.class );
this.widgetDescriptorService = mock( WidgetDescriptorService.class );
this.adminToolDescriptorService = mock( AdminToolDescriptorService.class );

this.handler = new WidgetApiHandler( this.controllerScriptFactory, this.widgetDescriptorService );
this.handler = new WidgetApiHandler( this.controllerScriptFactory, this.widgetDescriptorService, this.adminToolDescriptorService );
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.enonic.xp.admin.impl.tool;

import java.util.Set;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand All @@ -15,6 +17,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class XmlAdminToolDescriptorParserTest
extends XmlModelParserTest
Expand Down Expand Up @@ -73,6 +76,23 @@ public void testParseWithApis()
assertEquals( "", apiMountDescriptor4.getApiKey() );
}

@Test
public void testParseWithInterfaces()
throws Exception
{
parse( this.parser, "-interfaces.xml" );

final AdminToolDescriptor toolDescriptor = this.builder.build();

assertResult( toolDescriptor );

assertEquals( 2, toolDescriptor.getInterfaces().size() );

final Set<String> interfaces = toolDescriptor.getInterfaces();
assertTrue( interfaces.contains( "generic" ) );
assertTrue( interfaces.contains( "admin.dashboard" ) );
}

@Test
public void testParseWithInvalidApis()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<tool xmlns="urn:enonic:xp:model:1.0">
<display-name i18n="key.display-name">My admin tool</display-name>
<description i18n="key.description">Tool description</description>
<allow>
<principal>role:system.admin</principal>
</allow>
<interfaces>
<interface>generic</interface>
<interface>admin.dashboard</interface>
</interfaces>
</tool>
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,13 @@
</xs:complexType>
</xs:element>
<xs:element name="apis" minOccurs="0" type="apiMount"/>
<xs:element minOccurs="0" name="interfaces">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" name="interface" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>

Expand Down

0 comments on commit c392786

Please sign in to comment.