Merge branch 'stable-3.8'
* stable-3.8:
SamlWebFilterIT: throw Exception from tests
Do not set full name when not allowed by Realm
Do not swallow the user provisioning exception
Fix formatting issues
Do not load the SAML plugin when auth.httpDisplaynameHeader is set
Change-Id: I969be2417c0832cced13649a6462439601dc329c
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..f7df7d7 100644
--- a/README.md
+++ b/README.md
@@ -75,9 +75,12 @@
How to build the Gerrit Plugin API is described in the [Gerrit documentation](../../../Documentation/dev-bazel.html#_extension_and_plugin_api_jar_files).
### Configure Gerrit to use the SAML filter:
-In `$site_path/etc/gerrit.config` file, the `[httpd]` section should contain
+In `$site_path/etc/gerrit.config` file, the `[httpd]` and `[gerrit]` sections should
+contain:
```
+[gerrit]
+ installModule = com.googlesource.gerrit.plugins.saml.Module
[httpd]
filterClass = com.googlesource.gerrit.plugins.saml.SamlWebFilter
```
@@ -209,3 +212,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/simplesamlphp/README.md b/simplesamlphp/README.md
index 3f15824..4f598fb 100644
--- a/simplesamlphp/README.md
+++ b/simplesamlphp/README.md
@@ -48,7 +48,7 @@
autoUpdateAccountActiveStatus = true
[saml]
serviceProviderEntityId = gerritSaml
- keystorePath = /Users/d073103/sites/serviceuserMaster/etc/keystore
+ keystorePath = etc/keystore
keystorePassword = pac4j-demo-password
privateKeyPassword = pac4j-demo-password
metadataPath = http://localhost:8080/simplesaml/saml2/idp/metadata.php
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/AuthHeaders.java b/src/main/java/com/googlesource/gerrit/plugins/saml/AuthHeaders.java
new file mode 100644
index 0000000..e4e6848
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/saml/AuthHeaders.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2024 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;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface AuthHeaders {}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/LibModuleData.java b/src/main/java/com/googlesource/gerrit/plugins/saml/LibModuleData.java
new file mode 100644
index 0000000..c84fe7a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/saml/LibModuleData.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2024 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;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface LibModuleData {}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/Module.java b/src/main/java/com/googlesource/gerrit/plugins/saml/Module.java
new file mode 100644
index 0000000..a75bd72
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/saml/Module.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2024 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;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.saml.pgm.LibModuleDataDirUtil;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+import org.pac4j.saml.client.SAML2Client;
+
+public class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(SamlConfig.class);
+ bind(SAML2Client.class).toProvider(SamlClientProvider.class);
+ }
+
+ @Provides
+ @Singleton
+ @AuthHeaders
+ public Set<String> getAuthHeaders(AuthConfig auth) {
+ HashSet<String> authHeaders =
+ Sets.newHashSet(
+ auth.getLoginHttpHeader().toUpperCase(),
+ auth.getHttpEmailHeader().toUpperCase(),
+ auth.getHttpExternalIdHeader().toUpperCase());
+
+ if (authHeaders.contains("") || authHeaders.contains(null)) {
+ throw new ProvisionException("All authentication headers must be set.");
+ }
+
+ if (authHeaders.size() != 3) {
+ throw new ProvisionException(
+ "Unique values for httpUserNameHeader, "
+ + "httpEmailHeader and httpExternalIdHeader are required.");
+ }
+
+ return authHeaders;
+ }
+
+ @Provides
+ @LibModuleData
+ Path getLibModuleData(@SitePath Path sitePath) {
+ return LibModuleDataDirUtil.createLibModuleDataDir(sitePath);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlClientProvider.java b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlClientProvider.java
new file mode 100644
index 0000000..9d5d047
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlClientProvider.java
@@ -0,0 +1,84 @@
+// 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;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.googlesource.gerrit.plugins.saml.SamlWebFilter.SAML_CALLBACK;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.nio.file.Path;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.config.SAML2Configuration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class SamlClientProvider implements Provider<SAML2Client> {
+ private static final Logger log = LoggerFactory.getLogger(SamlClientProvider.class);
+
+ private final SamlConfig samlConfig;
+ private final String canonicalUrl;
+ private final Path libModuleDataDir;
+
+ @Inject
+ public SamlClientProvider(
+ @CanonicalWebUrl @Nullable String canonicalUrl,
+ SamlConfig samlConfig,
+ @LibModuleData Path libModuleDataDir) {
+ this.samlConfig = samlConfig;
+ this.canonicalUrl = canonicalUrl;
+ this.libModuleDataDir = libModuleDataDir;
+ }
+
+ @Override
+ public SAML2Client get() {
+ SAML2Configuration samlClientConfig =
+ new SAML2Configuration(
+ samlConfig.getKeystorePath(), samlConfig.getKeystorePassword(),
+ samlConfig.getPrivateKeyPassword(), samlConfig.getMetadataPath());
+
+ if (!Strings.isNullOrEmpty(samlConfig.getIdentityProviderEntityId())) {
+ if (!Strings.isNullOrEmpty(samlConfig.getServiceProviderEntityId())) {
+ log.warn(
+ "Both identityProviderEntityId as serviceProviderEntityId are set, ignoring serviceProviderEntityId.");
+ }
+ samlClientConfig.setIdentityProviderEntityId(samlConfig.getIdentityProviderEntityId());
+ } else {
+ samlClientConfig.setServiceProviderMetadataPath(getSpMetadataPath().toString());
+ if (!Strings.isNullOrEmpty(samlConfig.getServiceProviderEntityId())) {
+ samlClientConfig.setServiceProviderEntityId(samlConfig.getServiceProviderEntityId());
+ }
+ }
+
+ samlClientConfig.setUseNameQualifier(samlConfig.useNameQualifier());
+ samlClientConfig.setMaximumAuthenticationLifetime(samlConfig.getMaxAuthLifetimeAttr());
+
+ SAML2Client saml2Client = new SAML2Client(samlClientConfig);
+
+ checkNotNull(canonicalUrl, "gerrit.canonicalWebUrl must be set in gerrit.config");
+ saml2Client.setCallbackUrl(canonicalUrl + SAML_CALLBACK);
+
+ return saml2Client;
+ }
+
+ public Path getSpMetadataPath() {
+ return libModuleDataDir.resolve("sp-metadata.xml");
+ }
+}
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 45b26f3..96c591c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlConfig.java
@@ -15,6 +15,7 @@
package com.googlesource.gerrit.plugins.saml;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Config;
@@ -41,20 +42,21 @@
private final String memberOfAttr;
@Inject
- SamlConfig(@GerritServerConfig Config cfg) {
+ public SamlConfig(@GerritServerConfig Config cfg, SitePaths sitePaths) {
serviceProviderEntityId = getString(cfg, "serviceProviderEntityId");
identityProviderEntityId = getString(cfg, "identityProviderEntityId");
metadataPath = getString(cfg, "metadataPath");
- keystorePath = getString(cfg, "keystorePath");
+ keystorePath =
+ sitePaths.resolve(getStringWithDefault(cfg, "keystorePath", "etc/keystore")).toString();
privateKeyPassword = getString(cfg, "privateKeyPassword");
keystorePassword = getString(cfg, "keystorePassword");
- displayNameAttr = getGetStringWithDefault(cfg, "displayNameAttr", "DisplayName");
- userNameAttr = getGetStringWithDefault(cfg, "userNameAttr", "UserName");
- emailAddressAttr = getGetStringWithDefault(cfg, "emailAddressAttr", "EmailAddress");
+ displayNameAttr = getStringWithDefault(cfg, "displayNameAttr", "DisplayName");
+ userNameAttr = getStringWithDefault(cfg, "userNameAttr", "UserName");
+ emailAddressAttr = getStringWithDefault(cfg, "emailAddressAttr", "EmailAddress");
maxAuthLifetimeAttr = cfg.getInt("saml", "maxAuthLifetime", maxAuthLifetimeDefault);
computedDisplayName = cfg.getBoolean(SAML_SECTION, "computedDisplayName", false);
- firstNameAttr = getGetStringWithDefault(cfg, "firstNameAttr", "FirstName");
- lastNameAttr = getGetStringWithDefault(cfg, "lastNameAttr", "LastName");
+ firstNameAttr = getStringWithDefault(cfg, "firstNameAttr", "FirstName");
+ lastNameAttr = getStringWithDefault(cfg, "lastNameAttr", "LastName");
useNameQualifier = cfg.getBoolean(SAML_SECTION, "useNameQualifier", true);
memberOfAttr = getString(cfg, "memberOfAttr");
}
@@ -95,7 +97,7 @@
return cfg.getString(SAML_SECTION, null, name);
}
- private static String getGetStringWithDefault(Config cfg, String name, String defaultValue) {
+ private static String getStringWithDefault(Config cfg, String name, String defaultValue) {
String result = getString(cfg, name);
if (result != null) {
return result;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlMembership.java b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlMembership.java
index 1fd8a1f..5f8e959 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlMembership.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlMembership.java
@@ -22,12 +22,12 @@
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.*;
import com.google.gerrit.server.group.db.GroupDelta;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.group.db.InternalGroupCreation;
-import com.google.gerrit.server.notedb.Sequences;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlWebFilter.java b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlWebFilter.java
index 2a73b44..8eaf717 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/saml/SamlWebFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/saml/SamlWebFilter.java
@@ -14,13 +14,8 @@
package com.googlesource.gerrit.plugins.saml;
-import static com.google.common.base.Preconditions.checkNotNull;
-
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
import com.google.common.collect.Iterators;
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.Accounts;
@@ -29,16 +24,12 @@
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.OneOffRequestContext;
import com.google.inject.Inject;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
@@ -46,6 +37,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Objects;
+import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
@@ -61,7 +53,6 @@
import org.pac4j.core.exception.HttpAction;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.saml.client.SAML2Client;
-import org.pac4j.saml.config.SAML2Configuration;
import org.pac4j.saml.credentials.SAML2Credentials;
import org.pac4j.saml.profile.SAML2Profile;
import org.pac4j.saml.state.SAML2StateGenerator;
@@ -74,14 +65,14 @@
private static final String GERRIT_LOGOUT = "/logout";
@VisibleForTesting static final String GERRIT_LOGIN = "/login";
- private static final String SAML = "saml";
- private static final String SAML_CALLBACK = "plugins/" + SAML + "/callback";
+ public static final String SAML = "saml";
+ public static final String SAML_CALLBACK = "plugins/" + SAML + "/callback";
@VisibleForTesting static final String SESSION_ATTR_USER = "Gerrit-Saml-User";
private final SAML2Client saml2Client;
private final SamlConfig samlConfig;
private final AuthConfig auth;
- private final HashSet<String> authHeaders;
+ private final Set<String> authHeaders;
private final SamlMembership samlMembership;
private final GerritApi gApi;
private final Accounts accounts;
@@ -92,14 +83,13 @@
SamlWebFilter(
AuthConfig auth,
Realm realm,
- @CanonicalWebUrl @Nullable String canonicalUrl,
- SitePaths sitePaths,
SamlConfig samlConfig,
SamlMembership samlMembership,
+ @AuthHeaders Set<String> authHeaders,
GerritApi gApi,
Accounts accounts,
- OneOffRequestContext oneOffRequestContext)
- throws IOException {
+ SAML2Client saml2Client,
+ OneOffRequestContext oneOffRequestContext) {
this.auth = auth;
if (auth.getHttpDisplaynameHeader() != null) {
throw new ProvisionException(
@@ -110,45 +100,8 @@
this.realmAllowsFullNameEditing = realm.allowsEdit(AccountFieldName.FULL_NAME);
this.samlMembership = samlMembership;
log.debug("Max Authentication Lifetime: " + samlConfig.getMaxAuthLifetimeAttr());
- SAML2Configuration samlClientConfig =
- new SAML2Configuration(
- samlConfig.getKeystorePath(), samlConfig.getKeystorePassword(),
- samlConfig.getPrivateKeyPassword(), samlConfig.getMetadataPath());
-
- if (!Strings.isNullOrEmpty(samlConfig.getIdentityProviderEntityId())) {
- if (!Strings.isNullOrEmpty(samlConfig.getServiceProviderEntityId())) {
- log.warn(
- "Both identityProviderEntityId as serviceProviderEntityId are set, ignoring serviceProviderEntityId.");
- }
- samlClientConfig.setIdentityProviderEntityId(samlConfig.getIdentityProviderEntityId());
- } else {
- samlClientConfig.setServiceProviderMetadataPath(
- ensureExists(sitePaths.data_dir).resolve("sp-metadata.xml").toString());
- if (!Strings.isNullOrEmpty(samlConfig.getServiceProviderEntityId())) {
- samlClientConfig.setServiceProviderEntityId(samlConfig.getServiceProviderEntityId());
- }
- }
-
- samlClientConfig.setUseNameQualifier(samlConfig.useNameQualifier());
- samlClientConfig.setMaximumAuthenticationLifetime(samlConfig.getMaxAuthLifetimeAttr());
-
- saml2Client = new SAML2Client(samlClientConfig);
- authHeaders =
- Sets.newHashSet(
- auth.getLoginHttpHeader().toUpperCase(),
- auth.getHttpEmailHeader().toUpperCase(),
- auth.getHttpExternalIdHeader().toUpperCase());
- if (authHeaders.contains("") || authHeaders.contains(null)) {
- throw new RuntimeException("All authentication headers must be set.");
- }
- if (authHeaders.size() != 3) {
- throw new RuntimeException(
- "Unique values for httpUserNameHeader, "
- + "httpEmailHeader and httpExternalIdHeader are required.");
- }
- checkNotNull(canonicalUrl, "gerrit.canonicalWebUrl must be set in gerrit.config");
- saml2Client.setCallbackUrl(canonicalUrl + SAML_CALLBACK);
-
+ this.saml2Client = saml2Client;
+ this.authHeaders = authHeaders;
this.gApi = gApi;
this.accounts = accounts;
this.oneOffRequestContext = oneOffRequestContext;
@@ -337,10 +290,6 @@
return auth.isUserNameToLowerCase() ? username.toLowerCase(Locale.US) : username;
}
- private static Path ensureExists(Path dataDir) throws IOException {
- return Files.createDirectories(dataDir.resolve(SAML));
- }
-
private class AuthenticatedHttpRequest extends HttpServletRequestWrapper {
private AuthenticatedUser user;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/saml/pgm/LibModuleDataDirUtil.java b/src/main/java/com/googlesource/gerrit/plugins/saml/pgm/LibModuleDataDirUtil.java
new file mode 100644
index 0000000..699ad81
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/saml/pgm/LibModuleDataDirUtil.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2024 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 java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class LibModuleDataDirUtil {
+
+ public static Path createLibModuleDataDir(Path sitePath) {
+ Path dataDir = sitePath.resolve("data/saml");
+ if (!Files.isDirectory(dataDir)) {
+ try {
+ Files.createDirectories(dataDir);
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Cannot create %s", dataDir.toAbsolutePath()), e);
+ }
+ }
+ return dataDir;
+ }
+}
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..d9f6438
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/saml/pgm/SamlMetadataCreator.java
@@ -0,0 +1,105 @@
+// 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 static com.googlesource.gerrit.plugins.saml.pgm.LibModuleDataDirUtil.createLibModuleDataDir;
+
+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, new SamlConfig(cfg, sitePaths), createLibModuleDataDir(sitePath));
+ } 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);
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/saml/SamlWebFilterIT.java b/src/test/java/com/googlesource/gerrit/plugins/saml/SamlWebFilterIT.java
index 0b99d26..be9a7f7 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/saml/SamlWebFilterIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/saml/SamlWebFilterIT.java
@@ -34,16 +34,17 @@
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.util.OneOffRequestContext;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
import com.google.inject.Inject;
import com.google.inject.Injector;
+import com.google.inject.Module;
import com.google.inject.Provider;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletResponse;
@@ -96,6 +97,11 @@
assertThat(account.name).isEqualTo(samlDisplayName);
}
+ @Override
+ public Module createModule() {
+ return new com.googlesource.gerrit.plugins.saml.Module();
+ }
+
@Test
public void failAuthenticationWhenAccountManipulationFails() throws Exception {
SamlWebFilter samlWebFilter =
@@ -166,12 +172,12 @@
return new SamlWebFilter(
testInjector.getInstance(AuthConfig.class),
realm,
- "",
- testInjector.getInstance(SitePaths.class),
testInjector.getInstance(SamlConfig.class),
testInjector.getInstance(SamlMembership.class),
+ Collections.emptySet(),
gerritApi,
testInjector.getInstance(Accounts.class),
+ null,
testInjector.getInstance(OneOffRequestContext.class));
}