Add tool to create SAML metadata offline The SAML metadata file (`$SITE/data/saml/sp-metadata.xml`) will be created on the first login attempt, when the plugin has been installed. However, at that point authentication would fail until the identity provider was configured using the metadata file of Gerrit. To avoid this period in which authentication is not possible, the metadata can now be created offline. Change-Id: I0048f7fcf872fc24ed78c36efbf72e182197ccfd
diff --git a/BUILD b/BUILD index 36550aa..071aee0 100644 --- a/BUILD +++ b/BUILD
@@ -1,6 +1,37 @@ -load("//tools/bzl:plugin.bzl", "PLUGIN_TEST_DEPS", "gerrit_plugin") +load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS", "PLUGIN_TEST_DEPS", "gerrit_plugin") load("//tools/bzl:junit.bzl", "junit_tests") +SAML_DEPS = [ + "@commons-collections//jar", + "@commons-lang//jar", + "@cryptacular//jar", + "@joda-time//jar", + "@opensaml-core//jar", + "@opensaml-messaging-api//jar", + "@opensaml-messaging-impl//jar", + "@opensaml-profile-api//jar", + "@opensaml-profile-impl//jar", + "@opensaml-saml-api//jar", + "@opensaml-saml-impl//jar", + "@opensaml-security-api//jar", + "@opensaml-security-impl//jar", + "@opensaml-soap-api//jar", + "@opensaml-soap-impl//jar", + "@opensaml-storage-api//jar", + "@opensaml-storage-impl//jar", + "@opensaml-xmlsec-api//jar", + "@opensaml-xmlsec-impl//jar", + "@pac4j-core//jar", + "@pac4j-saml//jar", + "@santuario-xmlsec//jar", + "@shibboleth-utilities//jar", + "@shibboleth-xmlsectool//jar", + "@spring-core//jar", + "@stax2-api//jar", + "@velocity//jar", + "@woodstox-core//jar", +] + gerrit_plugin( name = "saml", srcs = glob(["src/main/java/**/*.java"]), @@ -8,36 +39,7 @@ "Gerrit-PluginName: saml", ], resources = glob(["src/main/resources/**"]), - deps = [ - "@commons-collections//jar", - "@commons-lang//jar", - "@cryptacular//jar", - "@joda-time//jar", - "@opensaml-core//jar", - "@opensaml-messaging-api//jar", - "@opensaml-messaging-impl//jar", - "@opensaml-profile-api//jar", - "@opensaml-profile-impl//jar", - "@opensaml-saml-api//jar", - "@opensaml-saml-impl//jar", - "@opensaml-security-api//jar", - "@opensaml-security-impl//jar", - "@opensaml-soap-api//jar", - "@opensaml-soap-impl//jar", - "@opensaml-storage-api//jar", - "@opensaml-storage-impl//jar", - "@opensaml-xmlsec-api//jar", - "@opensaml-xmlsec-impl//jar", - "@pac4j-core//jar", - "@pac4j-saml//jar", - "@santuario-xmlsec//jar", - "@shibboleth-utilities//jar", - "@shibboleth-xmlsectool//jar", - "@spring-core//jar", - "@stax2-api//jar", - "@velocity//jar", - "@woodstox-core//jar", - ], + deps = SAML_DEPS, ) junit_tests( @@ -49,3 +51,14 @@ "//javatests/com/google/gerrit/util/http/testutil", ], ) + +java_binary( + name = "SamlMetadataCreator", + srcs = glob([ + "src/main/java/com/googlesource/gerrit/plugins/saml/**/*.java", + ]), + main_class = "com.googlesource.gerrit.plugins.saml.pgm.SamlMetadataCreator", + deps = PLUGIN_DEPS + SAML_DEPS + [ + "@commons-io//jar", + ], +)
diff --git a/README.md b/README.md index e9fa614..84275f3 100644 --- a/README.md +++ b/README.md
@@ -209,3 +209,29 @@ **saml.useNameQualifier**: By SAML specification, the authentication request must not contain a NameQualifier, if the SP entity is in the format nameid-format:entity. However, some IdP require that information to be present. You can force a NameQualifier in the request with the useNameQualifier parameter. For ADFS 3.0 support, set this to `false`. Default is true. + +### Create SAML metadata offline + +The SAML metadata file (`$SITE/data/saml/sp-metadata.xml`) will be created on the +first login attempt, when the plugin has been installed. However, at that point +authentication would fail until the identity provider was configured using the +metadata file of Gerrit. + +To avoid this period in which authentication is not possible, the metadata can +be created offline. To do so, a separate java binary has to be built: + +```sh +bazelisk build //plugins/saml:SamlMetadataCreator_deploy.jar +``` + +The resulting jar-file can then be used to create the metadata file based on the +existing gerrit.config: + +```sh +bazel-bin/plugins/saml/SamlMetaDataCreator \ + -d $SITE \ # Path to the Gerrit site + --overwrite # Whether to overwrite any existing metadata file +``` + +The resulting metadata will be printed to standard out and stored at +`$SITE/data/saml/sp-metadata.xml`.
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl index 16b7dad..ed1ca14 100644 --- a/external_plugin_deps.bzl +++ b/external_plugin_deps.bzl
@@ -15,6 +15,12 @@ ) maven_jar( + name = "commons-io", + artifact = "commons-io:commons-io:2.4", + sha1 = "b1b6ea3b7e4aa4f492509a4952029cd8e48019ad", + ) + + maven_jar( name = "commons-lang", artifact = "commons-lang:commons-lang:2.6", sha1 = "0ce1edb914c94ebc388f086c6827e8bdeec71ac2",
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlClientProvider.java b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlClientProvider.java index f5e593d..51d2cd7 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlClientProvider.java +++ b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlClientProvider.java
@@ -63,8 +63,7 @@ } samlClientConfig.setIdentityProviderEntityId(samlConfig.getIdentityProviderEntityId()); } else { - samlClientConfig.setServiceProviderMetadataPath( - ensureExists(sitePaths.data_dir).resolve("sp-metadata.xml").toString()); + samlClientConfig.setServiceProviderMetadataPath(getSpMetadataPath().toString()); if (!Strings.isNullOrEmpty(samlConfig.getServiceProviderEntityId())) { samlClientConfig.setServiceProviderEntityId(samlConfig.getServiceProviderEntityId()); } @@ -81,6 +80,10 @@ return saml2Client; } + public Path getSpMetadataPath() { + return ensureExists(sitePaths.data_dir).resolve("sp-metadata.xml"); + } + private static Path ensureExists(Path dataDir) { try { return Files.createDirectories(dataDir.resolve(SAML));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlConfig.java b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlConfig.java index ce4a4ee..96c591c 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlConfig.java +++ b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlConfig.java
@@ -42,7 +42,7 @@ private final String memberOfAttr; @Inject - SamlConfig(@GerritServerConfig Config cfg, SitePaths sitePaths) { + public SamlConfig(@GerritServerConfig Config cfg, SitePaths sitePaths) { serviceProviderEntityId = getString(cfg, "serviceProviderEntityId"); identityProviderEntityId = getString(cfg, "identityProviderEntityId"); metadataPath = getString(cfg, "metadataPath");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/pgm/SamlMetadataCreator.java b/src/main/java/com/googlesource/gerrit/plugins/saml/pgm/SamlMetadataCreator.java new file mode 100644 index 0000000..33a39e4 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/saml/pgm/SamlMetadataCreator.java
@@ -0,0 +1,102 @@ +// Copyright (C) 2023 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.saml.pgm; + +import com.google.gerrit.server.config.SitePaths; +import com.googlesource.gerrit.plugins.saml.SamlClientProvider; +import com.googlesource.gerrit.plugins.saml.SamlConfig; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.ParserProperties; +import org.pac4j.saml.client.SAML2Client; + +public class SamlMetadataCreator { + @Option( + name = "--overwrite", + usage = "Overwrite existing metadata file. Otherwise, print existing.") + private boolean overwrite; + + @Option( + name = "--site-path", + aliases = {"-d"}, + usage = "Local directory containing site data") + void setSitePath(String path) { + sitePath = Paths.get(path).normalize(); + } + + private Path sitePath = Paths.get(".").toAbsolutePath(); + private SitePaths sitePaths; + private SamlClientProvider samlClientProvider; + + private void createSamlMetadata() throws IOException { + Path spMetadataPath = samlClientProvider.getSpMetadataPath(); + if (overwrite && spMetadataPath.toFile().exists()) { + Files.delete(spMetadataPath); + } + + SAML2Client saml2Client = samlClientProvider.get(); + + saml2Client.init(); + String spMetadata = saml2Client.getServiceProviderMetadataResolver().getMetadata(); + System.out.print(spMetadata); + } + + private Config parseGerritConfig() throws ConfigInvalidException, IOException { + Config baseConfig = new Config(); + baseConfig.fromText(Files.readString(sitePaths.gerrit_config)); + + Config cfg = new Config(baseConfig); + + if (sitePaths.secure_config.toFile().exists()) { + cfg.fromText(Files.readString(sitePaths.secure_config)); + } + + return cfg; + } + + public void run(String[] args) throws IOException, ConfigInvalidException { + CmdLineParser parser = new CmdLineParser(this, ParserProperties.defaults().withAtSyntax(false)); + try { + parser.parseArgument(args); + } catch (CmdLineException e) { + System.err.println(e.getMessage()); + parser.printUsage(System.err); + System.exit(1); + return; + } + + sitePaths = new SitePaths(sitePath); + try { + Config cfg = parseGerritConfig(); + String canonicalWebUrl = cfg.getString("gerrit", null, "canonicalWebUrl"); + samlClientProvider = + new SamlClientProvider(canonicalWebUrl, sitePaths, new SamlConfig(cfg, sitePaths)); + } catch (ConfigInvalidException | IOException e) { + throw new ConfigInvalidException("Unable to parse Gerrit's configuration.", e); + } + createSamlMetadata(); + } + + public static void main(String[] args) throws Exception { + new SamlMetadataCreator().run(args); + } +}