diff --git a/modules/gplazma2-multimap/pom.xml b/modules/gplazma2-multimap/pom.xml
index 9695ec3d832..1afe1fed0c0 100644
--- a/modules/gplazma2-multimap/pom.xml
+++ b/modules/gplazma2-multimap/pom.xml
@@ -38,5 +38,10 @@
jimfs
test
+
+ ch.qos.logback
+ logback-classic
+ test
+
diff --git a/modules/gplazma2-multimap/src/main/java/org/dcache/gplazma/plugins/GplazmaMultiMapFile.java b/modules/gplazma2-multimap/src/main/java/org/dcache/gplazma/plugins/GplazmaMultiMapFile.java
index 8eb650db347..1b5d4875185 100644
--- a/modules/gplazma2-multimap/src/main/java/org/dcache/gplazma/plugins/GplazmaMultiMapFile.java
+++ b/modules/gplazma2-multimap/src/main/java/org/dcache/gplazma/plugins/GplazmaMultiMapFile.java
@@ -6,6 +6,8 @@
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
@@ -44,6 +46,8 @@
public class GplazmaMultiMapFile {
+ private static final URI PLACEHOLDER_ISSUER = URI.create("https://unknown-issuer.invalid/");
+
/**
* Information about the principals that may be mapped.
*/
@@ -116,7 +120,36 @@ public PrincipalMatcher buildMatcher(String value)
UID("uid", UidPrincipal.class),
USER_NAME("username", UserNamePrincipal.class),
ENTITLEMENT("entitlement", EntitlementPrincipal.class),
- OP("op", OAuthProviderPrincipal.class),
+ OP("op", OAuthProviderPrincipal.class) {
+ @Override
+ public Principal buildPrincipal(String value)
+ throws GplazmaParseMapFileException {
+ List items = Splitter.on(':').limit(2).splitToList(value);
+ URI issuer;
+ if (items.size() == 1) { // Backwards compatibility
+ issuer = PLACEHOLDER_ISSUER;
+ LOGGER.warn("Please replace \"op:{}\" with \"op:{}:URL\""
+ + " where URL is the issuer URL (the 'iss' claim"
+ + " value).", value, value);
+ } else {
+ try {
+ issuer = new URI(items.get(1));
+ } catch (URISyntaxException e) {
+ throw new GplazmaParseMapFileException("Invalid URL: "
+ + e.getMessage());
+ }
+ }
+ var name = items.get(0);
+ return new OAuthProviderPrincipal(name, issuer);
+ }
+
+ @Override
+ public PrincipalMatcher buildMatcher(String value)
+ throws GplazmaParseMapFileException {
+ return (Principal p) -> p instanceof OAuthProviderPrincipal
+ && ((OAuthProviderPrincipal)p).getName().equals(value);
+ }
+ },
ROLES("roles", RolePrincipal.class);
private final String label;
diff --git a/modules/gplazma2-multimap/src/test/java/org/dcache/gplazma/plugins/GplazmaMultiMapFileTest.java b/modules/gplazma2-multimap/src/test/java/org/dcache/gplazma/plugins/GplazmaMultiMapFileTest.java
index c533e42043f..fbd77d75c23 100644
--- a/modules/gplazma2-multimap/src/test/java/org/dcache/gplazma/plugins/GplazmaMultiMapFileTest.java
+++ b/modules/gplazma2-multimap/src/test/java/org/dcache/gplazma/plugins/GplazmaMultiMapFileTest.java
@@ -8,6 +8,7 @@
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
+import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -22,14 +23,26 @@
import org.dcache.auth.FQANPrincipal;
import org.dcache.auth.GidPrincipal;
import org.dcache.auth.GroupNamePrincipal;
+import org.dcache.auth.OAuthProviderPrincipal;
import org.dcache.auth.OidcSubjectPrincipal;
import org.dcache.auth.OpenIdGroupPrincipal;
import org.dcache.auth.UidPrincipal;
import org.dcache.auth.UserNamePrincipal;
import org.globus.gsi.gssapi.jaas.GlobusPrincipal;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
import org.junit.Before;
import org.junit.Test;
-
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import static java.util.Objects.requireNonNull;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.slf4j.LoggerFactory;
public class GplazmaMultiMapFileTest {
@@ -37,6 +50,7 @@ public class GplazmaMultiMapFileTest {
private Set mappedPrincipals;
private Path config;
private List warnings;
+ private List logEvents;
@Before
public void setup() throws Exception {
@@ -46,6 +60,13 @@ public void setup() throws Exception {
mapFile = new GplazmaMultiMapFile(config);
warnings = new ArrayList<>();
mapFile.setWarningConsumer(warnings::add);
+
+ // Capture logging activity
+ Logger logger = (Logger) LoggerFactory.getLogger(GplazmaMultiMapFile.class);
+ ListAppender appender = new ListAppender<>();
+ appender.start();
+ logger.addAppender(appender);
+ logEvents = appender.list;
}
@Test
@@ -431,6 +452,71 @@ public void shouldMatchNonPrimarySpecificGidWithNonPrimaryGid() throws Exception
assertThat(mappedPrincipals, hasItem(new UidPrincipal(2000)));
}
+ @Test
+ public void shouldMatchOpWithSameName() throws Exception {
+ givenConfig("op:FOO uid:1000 gid:2000");
+
+ whenMapping(new OAuthProviderPrincipal("FOO", URI.create("https://my-op.example.org/")));
+
+ assertThat(warnings, is(empty()));
+ assertThat(mappedPrincipals, hasItem(new UidPrincipal(1000)));
+ assertThat(mappedPrincipals, hasItem(new GidPrincipal(2000, false)));
+ }
+
+ @Test
+ public void shouldNotMatchOpWithDifferentName() throws Exception {
+ givenConfig("op:FOO uid:1000 gid:2000");
+
+ whenMapping(new OAuthProviderPrincipal("BAR", URI.create("https://my-op.example.org/")));
+
+ assertThat(warnings, is(empty()));
+ assertThat(mappedPrincipals, is(empty()));
+ }
+
+ @Test
+ public void shouldNotMatchOpWithDn() throws Exception {
+ givenConfig("op:FOO uid:1000 gid:2000");
+
+ whenMapping(new GlobusPrincipal("\"dn:/C=DE/S=Hamburg/OU=desy.de/CN=Kermit The Frog\""));
+
+ assertThat(warnings, is(empty()));
+ assertThat(mappedPrincipals, is(empty()));
+ }
+
+
+ @Test
+ public void shouldAddOpWithoutIssuer() throws Exception {
+ givenConfig("email:kermit@dcache.org op:FOO");
+
+ whenMapping(new EmailAddressPrincipal("kermit@dcache.org"));
+
+ assertThat(logEvents, hasItem(new LogEventMatcher(Level.WARN,
+ allOf(containsString("op:FOO"),
+ containsString("'iss' claim value")))));
+
+ var opPrincipals = mappedPrincipals.stream()
+ .filter(OAuthProviderPrincipal.class::isInstance)
+ .toList();
+ // Avoid asserting the OP's placeholder issuer URL because we don't
+ // guarantee that value.
+ assertThat(opPrincipals.size(), equalTo(1));
+ var opPrincipal = opPrincipals.get(0);
+ assertThat(opPrincipal.getName(), is(equalTo("FOO")));
+ }
+
+ @Test
+ public void shouldAddOpWithIssuer() throws Exception {
+ givenConfig("email:kermit@dcache.org op:FOO:https://my-op.example.org/");
+
+ whenMapping(new EmailAddressPrincipal("kermit@dcache.org"));
+
+ assertThat(warnings, is(empty()));
+ assertThat(logEvents, not(hasItem(new LogEventMatcher(Level.WARN,
+ containsString("op:FOO")))));
+ assertThat(mappedPrincipals, hasItem(new OAuthProviderPrincipal("FOO",
+ URI.create("https://my-op.example.org/"))));
+ }
+
/*----------------------- Helpers -----------------------------*/
private void givenConfig(String mapping) throws Exception {
@@ -454,4 +540,30 @@ private void whenMapping(Principal principal) throws Exception {
.findFirst()
.orElse(Collections.emptySet());
}
+
+ /**
+ * A simple Hamcrest matcher that allows for assertions against Logback
+ * events.
+ */
+ private static class LogEventMatcher extends TypeSafeMatcher {
+ private final Level level;
+ private final Matcher messageMatcher;
+
+ public LogEventMatcher(Level level, Matcher messageMatcher) {
+ this.level = requireNonNull(level);
+ this.messageMatcher = requireNonNull(messageMatcher);
+ }
+
+ @Override
+ protected boolean matchesSafely(ILoggingEvent item) {
+ return item.getLevel() == level && messageMatcher.matches(item.getFormattedMessage());
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("a log entry logged at ").appendValue(level)
+ .appendText(" and with formatted message ")
+ .appendDescriptionOf(messageMatcher);
+ }
+ }
}
\ No newline at end of file