Merge branch 'stable-2.15'
* stable-2.15:
Bazel: Include eclipse-out directory in .bazelignore
Add explanatory comment to empty BUILD file(s)
Upgrade bazlets to latest stable-2.15 to build with 2.15.7 API
Upgrade bazlets to latest stable-2.14 to build with 2.14.17 API
Upgrade bazlets to latest stable-2.15 to build with 2.15.6 API
WORKSPACE: Make commented out local_path line spaces indent consistent
Upgrade bazlets to latest stable-2.14 to build with 2.14.16 API
Align Eclipse compiler settings with core Gerrit's
bazlets: Replace native.git_repository with skylark rule
Upgrade bazlets to latest stable-2.15 to build with 2.15.5 API
Upgrade bazlets to latest stable-2.14 to build with 2.14.15 API
Update bazlets to latest stable-2.15 to build with 2.15.4 API
Migrate (i.e. move) `tools/bazel.rc` to `.bazelrc`
Update bazlets to latest stable-2.14 to build with 2.14.14 API
Update bazlets to latest stable-2.14 to build with 2.14.13 API
Update bazlets to latest stable-2.14 to use 2.14.12 API
Allow login during readonly
Rename ssh commands from put/delete/get to enable/disable/status
ReadOnlyByHttpIT: Add assertion that REST call worked as expected
Add tests for enabling/disabling read-only mode by SSH command
Remove custom servlet and associated tests
Use RestApiServlet and SSH commands for plugin actions
Allow to switch read only mode on/off
Update bazlets to latest stable-2.14 to use 2.14.11 API
ReadOnly: Use String#equals to compare strings
Format bzl files with buildifier
Update bazlets to latest stable-2.14 to use 2.14.10 API
Update bazlets to latest revision on stable-2.14
Change-Id: I23c7d8af83de47982ebee122a1eeed1d4e08a009
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..30f1613
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+eclipse-out
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..40e022d
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,126 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnull.secondary=
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullable.secondary=
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.doc.comment.support=enabled
+org.eclipse.jdt.core.compiler.problem.APILeak=warning
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=ignore
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=warning
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled
+org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=private
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=enabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagDescription=return_tag
+org.eclipse.jdt.core.compiler.problem.missingJavadocTags=ignore
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsMethodTypeParameters=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=protected
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
+org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning
+org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled
+org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.processAnnotations=enabled
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/BUILD b/BUILD
index d4fda80..416a3b6 100644
--- a/BUILD
+++ b/BUILD
@@ -1,4 +1,10 @@
-load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load(
+ "//tools/bzl:plugin.bzl",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+ "gerrit_plugin",
+)
+load("//tools/bzl:junit.bzl", "junit_tests")
gerrit_plugin(
name = "readonly",
@@ -9,5 +15,23 @@
"Gerrit-SshModule: com.googlesource.gerrit.plugins.readonly.SshModule",
"Gerrit-HttpModule: com.googlesource.gerrit.plugins.readonly.HttpModule",
],
- resources = glob(["src/main/**/*"]),
+ resources = glob(["src/main/resources/**/*"]),
+)
+
+junit_tests(
+ name = "readonly_tests",
+ srcs = glob(["src/test/java/**/*.java"]),
+ resources = glob(["src/test/resources/**/*"]),
+ deps = [
+ ":readonly__plugin_test_deps",
+ ],
+)
+
+java_library(
+ name = "readonly__plugin_test_deps",
+ testonly = 1,
+ visibility = ["//visibility:public"],
+ exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+ ":readonly__plugin",
+ ],
)
diff --git a/bazlets.bzl b/bazlets.bzl
index e14e488..f089af4 100644
--- a/bazlets.bzl
+++ b/bazlets.bzl
@@ -1,17 +1,18 @@
+load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
+
NAME = "com_googlesource_gerrit_bazlets"
def load_bazlets(
- commit,
- local_path = None
- ):
- if not local_path:
- native.git_repository(
- name = NAME,
- remote = "https://gerrit.googlesource.com/bazlets",
- commit = commit,
- )
- else:
- native.local_repository(
- name = NAME,
- path = local_path,
- )
+ commit,
+ local_path = None):
+ if not local_path:
+ git_repository(
+ name = NAME,
+ remote = "https://gerrit.googlesource.com/bazlets",
+ commit = commit,
+ )
+ else:
+ native.local_repository(
+ name = NAME,
+ path = local_path,
+ )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommand.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommand.java
index 46cb254..c6736cd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommand.java
@@ -20,7 +20,7 @@
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-@CommandMetaData(name = "disable", description = "Disable ssh commands", runsAt = MASTER_OR_SLAVE)
+@CommandMetaData(name = "disabled", description = "Disable ssh commands", runsAt = MASTER_OR_SLAVE)
class DisableCommand extends SshCommand {
@Inject ReadOnlyConfig config;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java
index 7bab398..1e422c8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java
@@ -29,14 +29,19 @@
private static final Logger log = LoggerFactory.getLogger(DisableCommandInterceptor.class);
private static final String PATTERN = "^gerrit plugin (\\brm\\b|\\bremove\\b) %s$";
+ private final ReadOnlyState state;
private final String disableCommand;
private final List<Pattern> allowPatterns = new ArrayList<>();
private final List<String> allowPrefixes = new ArrayList<>();
@Inject
- DisableCommandInterceptor(@PluginName String pluginName, ReadOnlyConfig config) {
- this.disableCommand = pluginName + " disable";
+ DisableCommandInterceptor(
+ @PluginName String pluginName, ReadOnlyConfig config, ReadOnlyState state) {
+ this.state = state;
+ this.disableCommand = pluginName + " disabled";
allowPatterns.add(Pattern.compile(String.format(PATTERN, pluginName)));
+ // Allow all SSH commands from this plugin
+ allowPrefixes.add(pluginName);
for (String allow : config.allowSshCommands()) {
if (allow.startsWith("^")) {
allowPatterns.add(Pattern.compile(allow));
@@ -48,7 +53,8 @@
@Override
public String intercept(String in) {
- if (allowPrefixes.stream().anyMatch(p -> in.startsWith(p))
+ if (!state.isReadOnly()
+ || allowPrefixes.stream().anyMatch(p -> in.startsWith(p))
|| allowPatterns.stream().anyMatch(p -> p.matcher(in).matches())) {
return in;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableReadOnlyCommand.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableReadOnlyCommand.java
new file mode 100644
index 0000000..a64d0b1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableReadOnlyCommand.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.readonly;
+
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+
+@RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+@CommandMetaData(name = "disable", description = "Disable read only mode")
+class DisableReadOnlyCommand extends SshCommand {
+ @Inject ReadOnlyEndpoint.Delete delete;
+
+ @Override
+ protected void run() throws IOException {
+ delete.apply(null, null);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/EnableReadOnlyCommand.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/EnableReadOnlyCommand.java
new file mode 100644
index 0000000..322a9f9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/EnableReadOnlyCommand.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.readonly;
+
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+
+@RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+@CommandMetaData(name = "enable", description = "Enable read only mode")
+class EnableReadOnlyCommand extends SshCommand {
+ @Inject ReadOnlyEndpoint.Put put;
+
+ @Override
+ protected void run() throws IOException {
+ put.apply(null, null);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/GetReadOnlyStatusCommand.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/GetReadOnlyStatusCommand.java
new file mode 100644
index 0000000..06a41e5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/GetReadOnlyStatusCommand.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.readonly;
+
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+@RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+@CommandMetaData(name = "status", description = "Show read only mode state")
+class GetReadOnlyStatusCommand extends SshCommand {
+ @Inject ReadOnlyEndpoint.Get get;
+
+ @Override
+ protected void run() {
+ stdout.println(get.apply(null));
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/Module.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/Module.java
index 88f681a..69fa804 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/Module.java
@@ -14,7 +14,10 @@
package com.googlesource.gerrit.plugins.readonly;
+import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+
import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.inject.AbstractModule;
@@ -22,5 +25,14 @@
@Override
protected void configure() {
DynamicSet.bind(binder(), CommitValidationListener.class).to(ReadOnly.class);
+ install(
+ new RestApiModule() {
+ @Override
+ protected void configure() {
+ put(CONFIG_KIND, "readonly").to(ReadOnlyEndpoint.Put.class);
+ delete(CONFIG_KIND, "readonly").to(ReadOnlyEndpoint.Delete.class);
+ get(CONFIG_KIND, "readonly").to(ReadOnlyEndpoint.Get.class);
+ }
+ });
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnly.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnly.java
index c651474..f84101c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnly.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnly.java
@@ -16,6 +16,8 @@
import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.httpd.AllRequestFilter;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.validators.CommitValidationException;
@@ -35,32 +37,51 @@
@Singleton
class ReadOnly extends AllRequestFilter implements CommitValidationListener {
private static final String GIT_UPLOAD_PACK_PROTOCOL = "/git-upload-pack";
+ private static final String LOGIN_PREFIX = "/login";
+ private static final String LOGIN_INFIX = LOGIN_PREFIX + "/";
+
+ private final ReadOnlyState state;
private final ReadOnlyConfig config;
+ private final String endpoint;
@Inject
- ReadOnly(ReadOnlyConfig config) {
+ ReadOnly(ReadOnlyState state, ReadOnlyConfig config, @PluginName String pluginName) {
+ this.state = state;
this.config = config;
+ this.endpoint = String.format("/config/server/%s~readonly", pluginName);
}
@Override
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
- throw new CommitValidationException(config.message());
+ if (state.isReadOnly()) {
+ throw new CommitValidationException(config.message());
+ }
+ return ImmutableList.of();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
- if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse)) {
- String method = ((HttpServletRequest) request).getMethod();
- String uri = ((HttpServletRequest) request).getRequestURI();
- if ((method == "POST" && !uri.endsWith(GIT_UPLOAD_PACK_PROTOCOL))
- || method == "PUT"
- || method == "DELETE") {
- ((HttpServletResponse) response).sendError(SC_SERVICE_UNAVAILABLE, config.message());
- return;
- }
+ if (state.isReadOnly()
+ && request instanceof HttpServletRequest
+ && response instanceof HttpServletResponse
+ && shouldBlock((HttpServletRequest) request)) {
+ ((HttpServletResponse) response).sendError(SC_SERVICE_UNAVAILABLE, config.message());
+ return;
}
chain.doFilter(request, response);
}
+
+ private boolean shouldBlock(HttpServletRequest request) {
+ String method = request.getMethod();
+ String servletPath = request.getServletPath();
+ return !servletPath.endsWith(endpoint)
+ && (("POST".equals(method)
+ && !servletPath.endsWith(GIT_UPLOAD_PACK_PROTOCOL)
+ && !servletPath.equals(LOGIN_PREFIX)
+ && !servletPath.contains(LOGIN_INFIX))
+ || "PUT".equals(method)
+ || "DELETE".equals(method));
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyConfig.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyConfig.java
index ba87301..28ef6c5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyConfig.java
@@ -38,7 +38,7 @@
ReadOnlyConfig(PluginConfigFactory pluginConfigFactory, @PluginName String pluginName) {
Config cfg = pluginConfigFactory.getGlobalPluginConfig(pluginName);
this.message = firstNonNull(cfg.getString(pluginName, null, MESSAGE_KEY), DEFAULT_MESSAGE);
- allowSshCommands = ImmutableList.copyOf(cfg.getStringList(pluginName, null, SSH_ALLOW));
+ this.allowSshCommands = ImmutableList.copyOf(cfg.getStringList(pluginName, null, SSH_ALLOW));
}
String message() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyEndpoint.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyEndpoint.java
new file mode 100644
index 0000000..caabb57
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyEndpoint.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.readonly;
+
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
+
+import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+
+public class ReadOnlyEndpoint {
+ static class Input {}
+
+ @RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+ @Singleton
+ public static class Get implements RestReadView<ConfigResource> {
+ private final ReadOnlyState state;
+
+ @Inject
+ Get(ReadOnlyState state) {
+ this.state = state;
+ }
+
+ @Override
+ public String apply(ConfigResource resource) {
+ return state.isReadOnly() ? "on" : "off";
+ }
+ }
+
+ @RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+ @Singleton
+ public static class Put implements RestModifyView<ConfigResource, Input> {
+ private final ReadOnlyState state;
+
+ @Inject
+ Put(ReadOnlyState state) {
+ this.state = state;
+ }
+
+ @Override
+ public Response<String> apply(ConfigResource resource, Input input) throws IOException {
+ state.setReadOnly(true);
+ return Response.ok("");
+ }
+ }
+
+ @RequiresAnyCapability({ADMINISTRATE_SERVER, MAINTAIN_SERVER})
+ @Singleton
+ public static class Delete implements RestModifyView<ConfigResource, Input> {
+ private final ReadOnlyState state;
+
+ @Inject
+ Delete(ReadOnlyState state) {
+ this.state = state;
+ }
+
+ @Override
+ public Response<String> apply(ConfigResource resource, Input input) throws IOException {
+ state.setReadOnly(false);
+ return Response.ok("");
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyState.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyState.java
new file mode 100644
index 0000000..05c7a91
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyState.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.readonly;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+
+@Singleton
+public class ReadOnlyState {
+ private static final String GERRIT_READONLY = "gerrit.readonly";
+
+ private final File marker;
+
+ @Inject
+ ReadOnlyState(SitePaths sitePaths) {
+ this.marker = sitePaths.etc_dir.resolve(GERRIT_READONLY).toFile();
+ }
+
+ public boolean isReadOnly() {
+ return marker.exists();
+ }
+
+ public void setReadOnly(boolean readOnly) throws IOException {
+ if (readOnly && !marker.exists()) {
+ Files.newOutputStream(marker.toPath(), StandardOpenOption.CREATE).close();
+ } else if (!readOnly && marker.exists()) {
+ Files.delete(marker.toPath());
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/SshModule.java
index 8067516..357c2f7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/SshModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/SshModule.java
@@ -24,5 +24,14 @@
DynamicItem.bind(binder(), SshCreateCommandInterceptor.class)
.to(DisableCommandInterceptor.class);
command(DisableCommand.class);
+
+ command(EnableReadOnlyCommand.class);
+ alias("on", EnableReadOnlyCommand.class);
+
+ command(DisableReadOnlyCommand.class);
+ alias("off", DisableReadOnlyCommand.class);
+
+ command(GetReadOnlyStatusCommand.class);
+ alias("get", GetReadOnlyStatusCommand.class);
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/readonly/AbstractReadOnlyTest.java b/src/test/java/com/googlesource/gerrit/plugins/readonly/AbstractReadOnlyTest.java
new file mode 100644
index 0000000..f0bf1cf
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/readonly/AbstractReadOnlyTest.java
@@ -0,0 +1,165 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.readonly;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE;
+import static org.junit.Assert.fail;
+
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.server.change.PutTopic;
+import org.eclipse.jgit.api.errors.TransportException;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.junit.Ignore;
+import org.junit.Test;
+
+@Ignore
+@TestPlugin(
+ name = "readonly",
+ sysModule = "com.googlesource.gerrit.plugins.readonly.Module",
+ httpModule = "com.googlesource.gerrit.plugins.readonly.HttpModule",
+ sshModule = "com.googlesource.gerrit.plugins.readonly.SshModule")
+public abstract class AbstractReadOnlyTest extends LightweightPluginDaemonTest {
+ @Test
+ @UseLocalDisk
+ public void restRequestsAreRejectedWhenReadOnly() throws Exception {
+ ChangeInput in = new ChangeInput();
+ in.project = project.get();
+ in.branch = "master";
+ in.subject = "test";
+ ChangeInfo change = gApi.changes().create(in).get();
+
+ // GET should be allowed
+ String url = "/changes/" + change.changeId;
+ adminRestSession.get(url).assertOK();
+
+ // PUT should be allowed
+ PutTopic.Input topic = new PutTopic.Input();
+ topic.topic = "topic";
+ adminRestSession.put(url + "/topic", topic).assertOK();
+
+ // DELETE should be allowed
+ adminRestSession.delete(url + "/topic").assertNoContent();
+
+ // POST should be allowed
+ adminRestSession.post(url + "/abandon").assertOK();
+
+ // Enable read-only
+ setReadOnly(true);
+
+ // GET should be allowed
+ adminRestSession.get(url).assertOK();
+
+ // PUT should be blocked
+ adminRestSession.put(url + "/topic", topic).assertStatus(SC_SERVICE_UNAVAILABLE);
+
+ // DELETE should be blocked
+ adminRestSession.delete(url + "/topic").assertStatus(SC_SERVICE_UNAVAILABLE);
+
+ // POST should be blocked
+ adminRestSession.post(url + "/restore").assertStatus(SC_SERVICE_UNAVAILABLE);
+
+ // Disable read-only
+ setReadOnly(false);
+
+ // GET should be allowed
+ adminRestSession.get(url).assertOK();
+
+ // PUT should be allowed
+ adminRestSession.put(url + "/topic", topic).assertOK();
+
+ // DELETE should be allowed
+ adminRestSession.delete(url + "/topic").assertNoContent();
+
+ // POST should be allowed
+ adminRestSession.post(url + "/restore").assertOK();
+ }
+
+ @Test
+ @UseLocalDisk
+ @UseSsh
+ public void sshCommandsAreRejectedWhenReadOnly() throws Exception {
+ String command = "gerrit ls-projects";
+
+ // Command should succeed
+ adminSshSession.exec(command);
+ adminSshSession.assertSuccess();
+
+ // Enable read-only
+ setReadOnly(true);
+
+ // Command should be blocked
+ adminSshSession.exec(command);
+ adminSshSession.assertFailure("READ ONLY");
+
+ // Disable read-only
+ setReadOnly(false);
+
+ // Command should succeed
+ adminSshSession.exec(command);
+ adminSshSession.assertSuccess();
+ }
+
+ @Test
+ @UseLocalDisk
+ @UseSsh
+ public void pushBySshIsRejectedWhenReadOnly() throws Exception {
+ pushForReview(true);
+ }
+
+ @Test
+ @UseLocalDisk
+ public void pushByHttpIsRejectedWhenReadOnly() throws Exception {
+ pushForReview(false);
+ }
+
+ private void pushForReview(boolean ssh) throws Exception {
+ String url = ssh ? adminSshSession.getUrl() : admin.getHttpUrl(server);
+ if (!ssh) {
+ CredentialsProvider.setDefault(
+ new UsernamePasswordCredentialsProvider(admin.username, admin.httpPassword));
+ }
+ testRepo = GitUtil.cloneProject(project, url + "/" + project.get());
+
+ // Push should succeed
+ pushTo("refs/for/master").assertOkStatus();
+
+ // Enable read-only
+ setReadOnly(true);
+
+ // Push should fail
+ try {
+ pushTo("refs/for/master");
+ fail("expected TransportException");
+ } catch (TransportException e) {
+ assertThat(e).hasMessageThat().contains("READ ONLY");
+ }
+
+ // Disable read-only
+ setReadOnly(false);
+
+ // Push should succeed
+ pushTo("refs/for/master").assertOkStatus();
+ }
+
+ protected abstract void setReadOnly(boolean readOnly) throws Exception;
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyByHttpIT.java b/src/test/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyByHttpIT.java
new file mode 100644
index 0000000..43f241c
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyByHttpIT.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.readonly;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.RestResponse;
+
+public class ReadOnlyByHttpIT extends AbstractReadOnlyTest {
+ @Override
+ protected void setReadOnly(boolean readOnly) throws Exception {
+ if (readOnly) {
+ adminRestSession.put("/config/server/readonly~readonly").assertOK();
+ } else {
+ adminRestSession.delete("/config/server/readonly~readonly").assertOK();
+ }
+ RestResponse response = adminRestSession.get("/config/server/readonly~readonly");
+ response.assertOK();
+ String expectedStatus = readOnly ? "on" : "off";
+ assertThat(response.getEntityContent()).contains(expectedStatus);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyBySshIT.java b/src/test/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyBySshIT.java
new file mode 100644
index 0000000..83d6c68
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyBySshIT.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.readonly;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.UseSsh;
+
+@UseSsh
+public class ReadOnlyBySshIT extends AbstractReadOnlyTest {
+ @Override
+ protected void setReadOnly(boolean readOnly) throws Exception {
+ String command = readOnly ? "enable" : "disable";
+ String expectedStatus = readOnly ? "on" : "off";
+
+ adminSshSession.exec("readonly " + command);
+ adminSshSession.assertSuccess();
+
+ String result = adminSshSession.exec("readonly status");
+ assertThat(result).contains(expectedStatus);
+ }
+}
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
index e69de29..c5ed0b7 100644
--- a/tools/bzl/BUILD
+++ b/tools/bzl/BUILD
@@ -0,0 +1 @@
+# Empty file required by Bazel
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
index dfcbe9c..d5764f7 100644
--- a/tools/bzl/classpath.bzl
+++ b/tools/bzl/classpath.bzl
@@ -1,2 +1,4 @@
-load("@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
- "classpath_collector")
+load(
+ "@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+ "classpath_collector",
+)
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..3af7e58
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,4 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+ "junit_tests",
+)
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..2eabedb
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", "maven_jar")
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 956dd59..0b25d23 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,5 +1,6 @@
load(
"@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
- "gerrit_plugin",
"PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+ "gerrit_plugin",
)
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index 757747c..e9b0ad2 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -3,7 +3,8 @@
classpath_collector(
name = "main_classpath_collect",
- deps = PLUGIN_DEPS + [
- "//:readonly__plugin",
+ testonly = 1,
+ deps = [
+ "//:readonly__plugin_test_deps",
],
)