Merge "Allow plugins to declare project permissions"
diff --git a/java/com/google/gerrit/extensions/config/CapabilityDefinition.java b/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
index aafb583..c76ec6c 100644
--- a/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
+++ b/java/com/google/gerrit/extensions/config/CapabilityDefinition.java
@@ -18,7 +18,4 @@
/** Specifies a capability declared by a plugin. */
@ExtensionPoint
-public abstract class CapabilityDefinition {
- /** @return description of the capability. */
- public abstract String getDescription();
-}
+public abstract class CapabilityDefinition implements PluginPermissionDefinition {}
diff --git a/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java b/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java
new file mode 100644
index 0000000..11b1981
--- /dev/null
+++ b/java/com/google/gerrit/extensions/config/PluginPermissionDefinition.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.config;
+
+/** Specifies a permission declared by a plugin. */
+public interface PluginPermissionDefinition {
+ /**
+ * Gets the description of a permission declared by a plugin.
+ *
+ * @return description of the permission.
+ */
+ String getDescription();
+}
diff --git a/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java b/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java
new file mode 100644
index 0000000..d1d9f9e
--- /dev/null
+++ b/java/com/google/gerrit/extensions/config/PluginProjectPermissionDefinition.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Specifies a repository permission declared by a plugin. */
+@ExtensionPoint
+public abstract class PluginProjectPermissionDefinition implements PluginPermissionDefinition {}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 4158346..711efaf 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -29,6 +29,7 @@
import com.google.gerrit.extensions.config.DownloadScheme;
import com.google.gerrit.extensions.config.ExternalIncludedIn;
import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
import com.google.gerrit.extensions.events.AccountIndexedListener;
import com.google.gerrit.extensions.events.AgreementSignupListener;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
@@ -309,6 +310,7 @@
DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
DynamicSet.setOf(binder(), CacheRemovalListener.class);
DynamicMap.mapOf(binder(), CapabilityDefinition.class);
+ DynamicMap.mapOf(binder(), PluginProjectPermissionDefinition.class);
DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
DynamicSet.setOf(binder(), AssigneeChangedListener.class);
DynamicSet.setOf(binder(), ChangeAbandonedListener.class);
diff --git a/java/com/google/gerrit/server/permissions/PluginPermissionsUtil.java b/java/com/google/gerrit/server/permissions/PluginPermissionsUtil.java
new file mode 100644
index 0000000..bb6153c
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/PluginPermissionsUtil.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginPermissionDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.Extension;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.regex.Pattern;
+
+/** Utilities for plugin permissions. */
+@Singleton
+public class PluginPermissionsUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final String PLUGIN_NAME_PATTERN_STRING = "[a-zA-Z0-9-]+";
+
+ /**
+ * Name pattern for a plugin non-capability permission stored in the config file.
+ *
+ * <p>This pattern requires a plugin declared permission to have a name in the access section of
+ * {@code ProjectConfig} with a format like "plugin-{pluginName}-{permissionName}", which makes it
+ * easier to tell if a config name represents a plugin permission or not. Note "-" isn't clear
+ * enough for this purpose since some core permissions, e.g. "label-", also contain "-".
+ */
+ private static final Pattern PLUGIN_PERMISSION_NAME_IN_CONFIG_PATTERN =
+ Pattern.compile("^plugin-" + PLUGIN_NAME_PATTERN_STRING + "-[a-zA-Z]+$");
+
+ /** Name pattern for a Gerrit plugin. */
+ private static final Pattern PLUGIN_NAME_PATTERN =
+ Pattern.compile("^" + PLUGIN_NAME_PATTERN_STRING + "$");
+
+ private final DynamicMap<CapabilityDefinition> capabilityDefinitions;
+ private final DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions;
+
+ @Inject
+ PluginPermissionsUtil(
+ DynamicMap<CapabilityDefinition> capabilityDefinitions,
+ DynamicMap<PluginProjectPermissionDefinition> pluginProjectPermissionDefinitions) {
+ this.capabilityDefinitions = capabilityDefinitions;
+ this.pluginProjectPermissionDefinitions = pluginProjectPermissionDefinitions;
+ }
+
+ /**
+ * Collects all the plugin declared capabilities.
+ *
+ * @return a map of plugin declared capabilities with "pluginName" as its keys and
+ * "pluginName-{permissionName}" as its values.
+ */
+ public ImmutableMap<String, String> collectPluginCapabilities() {
+ return collectPermissions(capabilityDefinitions, "");
+ }
+
+ /**
+ * Collects all the plugin declared project permissions.
+ *
+ * @return a map of plugin declared project permissions with "{pluginName}" as its keys and
+ * "plugin-{pluginName}-{permissionName}" as its values.
+ */
+ public ImmutableMap<String, String> collectPluginProjectPermissions() {
+ return collectPermissions(pluginProjectPermissionDefinitions, "plugin-");
+ }
+
+ private static <T extends PluginPermissionDefinition>
+ ImmutableMap<String, String> collectPermissions(DynamicMap<T> definitions, String prefix) {
+ ImmutableMap.Builder<String, String> permissionIdNames = ImmutableMap.builder();
+
+ for (Extension<T> extension : definitions) {
+ String pluginName = extension.getPluginName();
+ if (!PLUGIN_NAME_PATTERN.matcher(pluginName).matches()) {
+ logger.atWarning().log(
+ "Plugin name '%s' must match '%s' to use permissions; rename the plugin",
+ pluginName, PLUGIN_NAME_PATTERN.pattern());
+ continue;
+ }
+
+ String id = prefix + pluginName + "-" + extension.getExportName();
+ permissionIdNames.put(id, extension.get().getDescription());
+ }
+
+ return permissionIdNames.build();
+ }
+
+ /**
+ * Checks if a given name matches the plugin declared permission name pattern for configs.
+ *
+ * @param name a config name which may stand for a plugin permission.
+ * @return whether the name matches the plugin permission name pattern for configs.
+ */
+ public static boolean isPluginPermission(String name) {
+ return PLUGIN_PERMISSION_NAME_IN_CONFIG_PATTERN.matcher(name).matches();
+ }
+}
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index c4ef32e..439e7c0 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -18,6 +18,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.common.data.Permission.isPermission;
import static com.google.gerrit.reviewdb.client.Project.DEFAULT_SUBMIT_TYPE;
+import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isPluginPermission;
import static java.util.stream.Collectors.toList;
import com.google.common.base.CharMatcher;
@@ -747,7 +748,7 @@
for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
for (String n : Splitter.on(EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN).split(varName)) {
n = convertLegacyPermission(n);
- if (isPermission(n)) {
+ if (isCoreOrPluginPermission(n)) {
as.getPermission(n, true).setExclusiveGroup(true);
}
}
@@ -755,7 +756,7 @@
for (String varName : rc.getNames(ACCESS, refName)) {
String convertedName = convertLegacyPermission(varName);
- if (isPermission(convertedName)) {
+ if (isCoreOrPluginPermission(convertedName)) {
Permission perm = as.getPermission(convertedName, true);
loadPermissionRules(
rc,
@@ -784,6 +785,12 @@
}
}
+ private boolean isCoreOrPluginPermission(String permission) {
+ // Since plugins are loaded dynamically, here we can't load all plugin permissions and verify
+ // their existence.
+ return isPermission(permission) || isPluginPermission(permission);
+ }
+
private boolean isValidRegex(String refPattern) {
try {
RefPattern.validateRegExp(refPattern);
@@ -1360,7 +1367,7 @@
}
for (String varName : rc.getNames(ACCESS, refName)) {
- if (isPermission(convertLegacyPermission(varName))
+ if (isCoreOrPluginPermission(convertLegacyPermission(varName))
&& !have.contains(varName.toLowerCase())) {
rc.unset(ACCESS, refName, varName);
}
diff --git a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
index fa9bfde..9bb2e6d 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
@@ -14,38 +14,32 @@
package com.google.gerrit.server.restapi.config;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+
import com.google.common.collect.ImmutableMap;
-import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.config.CapabilityDefinition;
-import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.config.CapabilityConstants;
import com.google.gerrit.server.config.ConfigResource;
import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PluginPermissionsUtil;
import com.google.inject.Inject;
-import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
-import java.util.regex.Pattern;
/** List capabilities visible to the calling user. */
@Singleton
public class ListCapabilities implements RestReadView<ConfigResource> {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
- private static final Pattern PLUGIN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$");
-
private final PermissionBackend permissionBackend;
- private final DynamicMap<CapabilityDefinition> pluginCapabilities;
+ private final PluginPermissionsUtil pluginPermissionsUtil;
@Inject
public ListCapabilities(
- PermissionBackend permissionBackend, DynamicMap<CapabilityDefinition> pluginCapabilities) {
+ PermissionBackend permissionBackend, PluginPermissionsUtil pluginPermissionsUtil) {
this.permissionBackend = permissionBackend;
- this.pluginCapabilities = pluginCapabilities;
+ this.pluginPermissionsUtil = pluginPermissionsUtil;
}
@Override
@@ -59,21 +53,16 @@
}
public Map<String, CapabilityInfo> collectPluginCapabilities() {
- Map<String, CapabilityInfo> output = new HashMap<>();
- for (String pluginName : pluginCapabilities.plugins()) {
- if (!PLUGIN_NAME_PATTERN.matcher(pluginName).matches()) {
- logger.atWarning().log(
- "Plugin name '%s' must match '%s' to use capabilities; rename the plugin",
- pluginName, PLUGIN_NAME_PATTERN.pattern());
- continue;
- }
- for (Map.Entry<String, Provider<CapabilityDefinition>> entry :
- pluginCapabilities.byPlugin(pluginName).entrySet()) {
- String id = String.format("%s-%s", pluginName, entry.getKey());
- output.put(id, new CapabilityInfo(id, entry.getValue().get().getDescription()));
- }
- }
- return output;
+ return convertToPermissionInfos(pluginPermissionsUtil.collectPluginCapabilities());
+ }
+
+ private static ImmutableMap<String, CapabilityInfo> convertToPermissionInfos(
+ ImmutableMap<String, String> permissionIdNames) {
+ return permissionIdNames
+ .entrySet()
+ .stream()
+ .collect(
+ toImmutableMap(Map.Entry::getKey, e -> new CapabilityInfo(e.getKey(), e.getValue())));
}
private Map<String, CapabilityInfo> collectCoreCapabilities()
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java
index 1e6afa8..617883f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/PluginAccessIT.java
@@ -26,15 +26,20 @@
import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.permissions.PluginPermissionsUtil;
import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
import com.google.inject.Module;
import org.junit.Test;
public class PluginAccessIT extends AbstractDaemonTest {
+ private static final String TEST_PLUGIN_NAME = "gerrit";
+ private static final String TEST_PLUGIN_CAPABILITY = "aPluginCapability";
+ private static final String TEST_PLUGIN_PROJECT_PERMISSION = "aPluginProjectPermission";
- private static final String CORE_PLUGIN_PREFIX = "gerrit-";
- private static final String PLUGIN_CAPABILITY = "printHello";
+ @Inject PluginPermissionsUtil pluginPermissionsUtil;
@Override
public Module createModule() {
@@ -42,12 +47,21 @@
@Override
protected void configure() {
bind(CapabilityDefinition.class)
- .annotatedWith(Exports.named(PLUGIN_CAPABILITY))
+ .annotatedWith(Exports.named(TEST_PLUGIN_CAPABILITY))
.toInstance(
new CapabilityDefinition() {
@Override
public String getDescription() {
- return "Print Hello";
+ return "A Plugin Capability";
+ }
+ });
+ bind(PluginProjectPermissionDefinition.class)
+ .annotatedWith(Exports.named(TEST_PLUGIN_PROJECT_PERMISSION))
+ .toInstance(
+ new PluginProjectPermissionDefinition() {
+ @Override
+ public String getDescription() {
+ return "A Plugin Project Permission";
}
});
}
@@ -56,23 +70,39 @@
@Test
public void addPluginCapability() throws Exception {
- ProjectAccessInput accessInput = new ProjectAccessInput();
- AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
- PermissionInfo email = new PermissionInfo(null, null);
- PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ addPluginPermission(AccessSection.GLOBAL_CAPABILITIES, TEST_PLUGIN_CAPABILITY);
- email.rules = ImmutableMap.of(SystemGroupBackend.REGISTERED_USERS.get(), pri);
- accessSectionInfo.permissions = ImmutableMap.of(CORE_PLUGIN_PREFIX + PLUGIN_CAPABILITY, email);
- accessInput.add = ImmutableMap.of(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
+ // Verifies the plugin defined capability could be listed.
+ assertThat(pluginPermissionsUtil.collectPluginCapabilities())
+ .containsKey(TEST_PLUGIN_NAME + "-" + TEST_PLUGIN_CAPABILITY);
+ }
+
+ @Test
+ public void addPluginProjectPermission() throws Exception {
+ addPluginPermission("refs/heads/plugin-permission", TEST_PLUGIN_PROJECT_PERMISSION);
+
+ // Verifies the plugin defined capability could be listed.
+ assertThat(pluginPermissionsUtil.collectPluginProjectPermissions())
+ .containsKey("plugin-" + TEST_PLUGIN_NAME + "-" + TEST_PLUGIN_PROJECT_PERMISSION);
+ }
+
+ private void addPluginPermission(String accessSection, String permission) throws Exception {
+ ProjectAccessInput accessInput = new ProjectAccessInput();
+ PermissionRuleInfo ruleInfo = new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false);
+ PermissionInfo email = new PermissionInfo(null, null);
+ email.rules = ImmutableMap.of(SystemGroupBackend.REGISTERED_USERS.get(), ruleInfo);
+ String permissionConfigName = TEST_PLUGIN_NAME + "-" + permission;
+ if (!accessSection.equals(AccessSection.GLOBAL_CAPABILITIES)) {
+ permissionConfigName = "plugin-" + permissionConfigName;
+ }
+ AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
+ accessSectionInfo.permissions = ImmutableMap.of(permissionConfigName, email);
+ accessInput.add = ImmutableMap.of(accessSection, accessSectionInfo);
ProjectAccessInfo updatedAccessSectionInfo =
gApi.projects().name(allProjects.get()).access(accessInput);
- assertThat(
- updatedAccessSectionInfo
- .local
- .get(AccessSection.GLOBAL_CAPABILITIES)
- .permissions
- .keySet())
+
+ assertThat(updatedAccessSectionInfo.local.get(accessSection).permissions.keySet())
.containsAllIn(accessSectionInfo.permissions.keySet());
}
}
diff --git a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
index 081a2f7..30fabdc 100644
--- a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
+++ b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -16,9 +16,11 @@
import static com.google.common.truth.Truth.assertThat;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser;
@@ -44,8 +46,18 @@
@Override
protected void configure() {
DynamicMap.mapOf(binder(), CapabilityDefinition.class);
+ DynamicMap.mapOf(binder(), PluginProjectPermissionDefinition.class);
bind(CapabilityDefinition.class)
- .annotatedWith(Exports.named("printHello"))
+ .annotatedWith(Exports.named("foo"))
+ .toInstance(
+ new CapabilityDefinition() {
+ @Override
+ public String getDescription() {
+ return "Print Hello";
+ }
+ });
+ bind(CapabilityDefinition.class)
+ .annotatedWith(Exports.named("bar"))
.toInstance(
new CapabilityDefinition() {
@Override
@@ -69,10 +81,11 @@
assertThat(m.get(id).name).isNotNull();
}
- String pluginCapability = "gerrit-printHello";
- assertThat(m).containsKey(pluginCapability);
- assertThat(m.get(pluginCapability).id).isEqualTo(pluginCapability);
- assertThat(m.get(pluginCapability).name).isEqualTo("Print Hello");
+ for (String pluginCapability : ImmutableSet.of("gerrit-foo", "gerrit-bar")) {
+ assertThat(m).containsKey(pluginCapability);
+ assertThat(m.get(pluginCapability).id).isEqualTo(pluginCapability);
+ assertThat(m.get(pluginCapability).name).isEqualTo("Print Hello");
+ }
}
@Singleton
diff --git a/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java b/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java
new file mode 100644
index 0000000..fd68da3
--- /dev/null
+++ b/javatests/com/google/gerrit/server/permissions/PluginPermissionsUtilTest.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isPluginPermission;
+
+import org.junit.Test;
+
+/** Small tests for {@link PluginPermissionsUtil}. */
+public class PluginPermissionsUtilTest {
+
+ @Test
+ public void pluginPermissionNameInConfigPattern() {
+ assertThat(isPluginPermission("create")).isFalse();
+ assertThat(isPluginPermission("label-Code-Review")).isFalse();
+ assertThat(isPluginPermission("plugin-foo")).isFalse();
+ assertThat(isPluginPermission("plugin-foo")).isFalse();
+
+ assertThat(isPluginPermission("plugin-foo-a")).isTrue();
+ // "-" is allowed for a plugin name. Here "foo-a" should be the name of the plugin.
+ assertThat(isPluginPermission("plugin-foo-a-b")).isTrue();
+
+ assertThat(isPluginPermission("plugin-foo-a-")).isFalse();
+ assertThat(isPluginPermission("plugin-foo-a1")).isFalse();
+ }
+}