From 417dffc100ac4c4c4eeeea98fdfec17141443fe5 Mon Sep 17 00:00:00 2001 From: Paul Millar Date: Fri, 12 Apr 2024 09:16:27 +0200 Subject: [PATCH] gplazma alise initial version of plugin Motivation: A new service has been developed within the interTwin project: ALISE. An ALISE service supports account linking; i.e., it allows a facility's users to register their federated identity against their facility-local identity. This service is intended for situation where the facility's IAM solution cannot easily be updated to support such federated account linking. ALISE works by the OIDC service (dCache) querying ALISE's REST API, providing the user's identity ('sub' claim and the issuer URI). The response is the user's facility username and possibly a display name. Modification: A new gPlazma module is added that targets the ALISE service. New gplazma configuraiton is added to support this plugin. Result: Without configuration changes, there is no user- or admin observable changes. This patch adds the integration possibility where dCache login queries an ALISE service to learn a user's username. Target: master Requires-notes: yes Requires-book: yes Request: 10.1 Request: 10.0 Request: 9.2 --- .../org/dcache/util/PrincipalSetMaker.java | 27 ++ modules/gplazma2-alise/pom.xml | 64 +++ .../gplazma/alise/AliseLookupAgent.java | 220 +++++++++++ .../org/dcache/gplazma/alise/AlisePlugin.java | 190 +++++++++ .../gplazma/alise/CachingLookupAgent.java | 88 +++++ .../org/dcache/gplazma/alise/Identity.java | 63 +++ .../org/dcache/gplazma/alise/LookupAgent.java | 47 +++ .../gplazma/alise/AliseLookupAgentTest.java | 363 ++++++++++++++++++ .../dcache/gplazma/alise/AlisePluginTest.java | 262 +++++++++++++ packages/pom.xml | 5 + pom.xml | 1 + skel/share/defaults/gplazma.properties | 36 +- 12 files changed, 1364 insertions(+), 2 deletions(-) create mode 100644 modules/gplazma2-alise/pom.xml create mode 100644 modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/AliseLookupAgent.java create mode 100644 modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/AlisePlugin.java create mode 100644 modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/CachingLookupAgent.java create mode 100644 modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/Identity.java create mode 100644 modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/LookupAgent.java create mode 100644 modules/gplazma2-alise/src/test/java/org/dcache/gplazma/alise/AliseLookupAgentTest.java create mode 100644 modules/gplazma2-alise/src/test/java/org/dcache/gplazma/alise/AlisePluginTest.java diff --git a/modules/common/src/main/java/org/dcache/util/PrincipalSetMaker.java b/modules/common/src/main/java/org/dcache/util/PrincipalSetMaker.java index 80d4f7bb07e..0282911a65c 100644 --- a/modules/common/src/main/java/org/dcache/util/PrincipalSetMaker.java +++ b/modules/common/src/main/java/org/dcache/util/PrincipalSetMaker.java @@ -1,6 +1,7 @@ package org.dcache.util; import com.google.common.collect.Sets; +import java.net.URI; import java.security.Principal; import java.util.Collections; import java.util.Set; @@ -9,8 +10,10 @@ import org.dcache.auth.EmailAddressPrincipal; import org.dcache.auth.ExemptFromNamespaceChecks; import org.dcache.auth.FQANPrincipal; +import org.dcache.auth.FullNamePrincipal; import org.dcache.auth.GidPrincipal; import org.dcache.auth.GroupNamePrincipal; +import org.dcache.auth.OAuthProviderPrincipal; import org.dcache.auth.OidcSubjectPrincipal; import org.dcache.auth.UidPrincipal; import org.dcache.auth.UserNamePrincipal; @@ -52,12 +55,25 @@ public PrincipalSetMaker withUid(int uid) { * Add a username Principal to the set. * * @param name the username to add + * */ public PrincipalSetMaker withUsername(String username) { _principals.add(new UserNamePrincipal(username)); return this; } + /** + * Add a Full Name Principal to the set. + * + * @param name the full name of the user. + * + */ + public PrincipalSetMaker withFullname(String name) { + _principals.add(new FullNamePrincipal(name)); + return this; + } + + /** * Add a primary groupname Principal to the set. * @@ -152,6 +168,17 @@ public PrincipalSetMaker withOidc(String sub, String op) { return this; } + /** + * Add an OAuth2 Provider (OP) to the set. + * + * @param alias the name/alias of this OAuth2 Provider. + * @param uri the URI identity of the OAuth2 Provider. + */ + public PrincipalSetMaker withOauth2Provider(String alias, URI uri) { + _principals.add(new OAuthProviderPrincipal(alias, uri)); + return this; + } + /** * Add an Email principal to the set. * diff --git a/modules/gplazma2-alise/pom.xml b/modules/gplazma2-alise/pom.xml new file mode 100644 index 00000000000..1b7eb2a8f2d --- /dev/null +++ b/modules/gplazma2-alise/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + + org.dcache + dcache-parent + 10.2.0-SNAPSHOT + ../../pom.xml + + + gplazma2-alise + jar + + gPlazma 2 ALISE plugin + + + + org.slf4j + slf4j-api + + + org.slf4j + jcl-over-slf4j + + + com.google.guava + guava + + + org.dcache + dcache-common + ${project.version} + + + org.dcache + gplazma2 + ${project.version} + + + org.apache.httpcomponents + httpclient + + + com.fasterxml.jackson.core + jackson-databind + + + junit + junit + test + + + org.hamcrest + hamcrest + test + + + com.github.npathai + hamcrest-optional + test + + + diff --git a/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/AliseLookupAgent.java b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/AliseLookupAgent.java new file mode 100644 index 00000000000..c1e170b97aa --- /dev/null +++ b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/AliseLookupAgent.java @@ -0,0 +1,220 @@ +/* + * dCache - http://www.dcache.org/ + * + * Copyright (C) 2024 Deutsches Elektronen-Synchrotron + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.dcache.gplazma.alise; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.hash.Hashing; +import com.google.common.net.PercentEscaper; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import static java.util.Objects.requireNonNull; +import java.util.Optional; +import org.dcache.auth.FullNamePrincipal; +import org.dcache.auth.UserNamePrincipal; +import org.dcache.util.Result; + +/** + * Make an HTTP request to ALISE to discover the local identity of a user. + */ +public class AliseLookupAgent implements LookupAgent { + + private final String apikey; + private final HttpClient client; + private final URI endpoint; + private final String target; + private final Duration timeout; + + public AliseLookupAgent(URI endpoint, String target, String apikey, + String timeout) { + this(HttpClient.newHttpClient(), endpoint, target, apikey, timeout); + } + + @VisibleForTesting + AliseLookupAgent(HttpClient client, URI endpoint, String target, + String apikey, String timeout) { + this.client = requireNonNull(client); + this.apikey = requireNonNull(apikey); + this.endpoint = requireNonNull(endpoint); + this.target = requireNonNull(target); + this.timeout = Duration.parse(timeout); + } + + private URI buildQueryUrl(Identity identity) { + URI issuer = identity.issuer(); + var issuerHash = Hashing.sha1().hashString(issuer.toASCIIString(), StandardCharsets.UTF_8).toString(); + + String subject = identity.sub(); + var encodedSub = new PercentEscaper(".", false).escape(subject); + + String relPath = "api/v1/target/" + target + "/mapping/issuer/" + issuerHash + "/user/" + encodedSub + "?apikey=" + apikey; + return endpoint.resolve(relPath); + } + + @Override + public Result,String> lookup(Identity identity) { + URI queryUrl = buildQueryUrl(identity); + var request = HttpRequest.newBuilder(queryUrl).timeout(timeout).build(); + + try { + var response = client.send(request, BodyHandlers.ofString()); + return resultFromResponse(response); + } catch (InterruptedException | IOException e) { + return Result.failure("problem communicating with ALISE server: " + + e.toString()); + } + } + + private Result, String> resultFromResponse(HttpResponse response) { + Optional contentType = response.headers().firstValue("Content-Type"); + if (contentType.isPresent()) { + String mediaType = contentType.get(); + if (!mediaType.equals("application/json")) { + return Result.failure("Response not JSON (" + mediaType + ")"); + } + } + + JsonNode json; + try { + ObjectMapper mapper = new ObjectMapper(); + json = mapper.readValue(response.body(), JsonNode.class); + } catch (JsonProcessingException e) { + return Result.failure("Bad JSON in response: " + e.getMessage()); + } + + if (response.statusCode() != 200) { + String message = buildErrorMessage(json); + return Result.failure("ALISE reported a problem: " + message); + } + + return resultFromSuccessfulHttpRequest(json); + } + + private String buildErrorMessage(JsonNode body) { + if (body.has("message")) { + JsonNode messageNode = body.get("message"); + if (messageNode.isTextual()) { + return messageNode.asText(); + } else { + return "unknown (\"message\" field is not textual)"; + } + } + + if (body.has("detail")) { + JsonNode detailArrayNode = body.get("detail"); + if (!detailArrayNode.isArray()) { + return "unknown (\"detail\" not array)"; + } + + StringBuilder sb = new StringBuilder(); + for (JsonNode detail : detailArrayNode) { + if (sb.length() != 0) { + sb.append(", "); + } + + if (!detail.isObject()) { + sb.append("unknown (\"detail\" item not object)"); + continue; + } + + if (detail.has("msg")) { + JsonNode msgNode = detail.get("msg"); + if (msgNode.isTextual()) { + sb.append(msgNode.asText()); + } else { + sb.append("unknown (").append(msgNode).append(')'); + } + } else { + sb.append("unknown (no \"msg\" field)"); + } + + if (detail.has("loc")) { + JsonNode locArrayNode = detail.get("loc"); + if (locArrayNode.isArray()) { + sb.append('['); + boolean haveFirst = false; + for (JsonNode locNode : locArrayNode) { + if (haveFirst) { + sb.append(", "); + } + if (locNode.isTextual()) { + sb.append(locNode.asText()); + } else { + sb.append("unknown (").append(locNode).append(')'); + } + haveFirst = true; + } + sb.append(']'); + } + } + } + return sb.toString(); + } else { + return "Unknown problem"; + } + } + + private Result, String> resultFromSuccessfulHttpRequest(JsonNode json) { + if (!json.isObject()) { + return Result.failure("lookup not JSON object"); + } + + if (!json.has("internal")) { + return Result.failure("lookup missing \"internal\" field"); + } + + JsonNode internalNode = json.get("internal"); + if (!internalNode.isObject()) { + return Result.failure("\"internal\" field is not object"); + } + + if (!internalNode.has("username")) { + return Result.failure("\"internal\" field missing \"username\" field"); + } + + JsonNode usernameNode = internalNode.get("username"); + if (!usernameNode.isTextual()) { + return Result.failure("Non-textual \"username\" field"); + } + + List principals = new ArrayList<>(2); + principals.add(new UserNamePrincipal(usernameNode.asText())); + + if (internalNode.has("display_name")) { + JsonNode displayNameNode = internalNode.get("display_name"); + if (displayNameNode.isTextual()) { + principals.add(new FullNamePrincipal(displayNameNode.asText())); + } + } + + return Result.success(principals); + } +} diff --git a/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/AlisePlugin.java b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/AlisePlugin.java new file mode 100644 index 00000000000..55683cdd437 --- /dev/null +++ b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/AlisePlugin.java @@ -0,0 +1,190 @@ +/* + * dCache - http://www.dcache.org/ + * + * Copyright (C) 2024 Deutsches Elektronen-Synchrotron + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.dcache.gplazma.alise; + +import com.google.common.annotations.VisibleForTesting; +import static com.google.common.base.Preconditions.checkArgument; +import com.google.common.base.Splitter; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.dcache.auth.OAuthProviderPrincipal; +import org.dcache.auth.OidcSubjectPrincipal; +import org.dcache.gplazma.AuthenticationException; +import org.dcache.gplazma.plugins.GPlazmaMappingPlugin; +import org.dcache.util.Result; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static java.util.Objects.requireNonNull; + +/** + * A mapping plugin that makes use of the Account LInking SErvice: ALISE. + *

+ * ALISE allows a user to link their federated identity (an OIDC issuer + 'sub' + * claim) and their site-local user identity (expressed as a username). In + * addition, ALISE returns other information, such as the user's display name. + *

+ * ALISE works by allowing external services (such as dCache) to query its + * internal database of mapped identity with an issuer and a sub-claim value. + * If the query is successful then information about that user is returned, + * otherwise an error is returned. These queries require an API KEY. + *

+ * For further details, see the project's + * GitHub page + *

+ * TODO: ALISE provides a list of issuers it accepts. This list is available + * via the ${ALISE}/alise/supported_issuers endpoint. A future version of this + * plugin could maintain a (dCache-internal) cached copy of this list and use + * it to avoid sending ALISE tokens that it does not support. + */ +public class AlisePlugin implements GPlazmaMappingPlugin { + private static final Logger LOGGER = LoggerFactory.getLogger(AlisePlugin.class); + + private final List mappableIssuers; + private final LookupAgent lookupAgent; + + @Nonnull + private static String getRequiredProperty(Properties config, String key) { + var value = config.getProperty(key); + checkArgument(value != null, "Config property \"%s\" is missing.", key); + return value; + } + + private static LookupAgent buildLookupAgent(Properties config) + throws URISyntaxException { + var configEndpoint = getRequiredProperty(config,"gplazma.alise.endpoint"); + var endpoint = new URI(configEndpoint); + var apikey = getRequiredProperty(config,"gplazma.alise.apikey"); + var target = getRequiredProperty(config, "gplazma.alise.target"); + var timeout = getRequiredProperty(config, "gplazma.alise.timeout"); + LookupAgent alise = new AliseLookupAgent(endpoint, target, apikey, timeout); + + LookupAgent cache = new CachingLookupAgent(alise); + + return cache; + } + + public AlisePlugin(Properties config) throws URISyntaxException { + this(config, buildLookupAgent(config)); + } + + @VisibleForTesting + AlisePlugin(Properties config, LookupAgent agent) { + var issuerList = getRequiredProperty(config, "gplazma.alise.issuers"); + mappableIssuers = Splitter.on(' ').omitEmptyStrings().splitToList(issuerList); + lookupAgent = requireNonNull(agent); + } + + private Optional toIdentity(OidcSubjectPrincipal principal, + Map issuers) { + String issuerAlias = principal.getOP(); + URI issuer = issuers.get(issuerAlias); + if (issuer == null) { + // This is probably a bug, skip this identity. + LOGGER.warn("sub from issuer {}, but no corresponding OAuthProviderPrincipal", + issuerAlias); + return Optional.empty(); + } + + if (!mappableIssuers.isEmpty() + && !mappableIssuers.contains(issuerAlias) + && !mappableIssuers.contains(issuer.toASCIIString())) { + return Optional.empty(); + } + + return Optional.of(new Identity(issuer, principal.getSubClaim())); + } + + private Result,String> buildIdentityList(Collection principals) + throws AuthenticationException { + var allSubs = principals.stream() + .filter(OidcSubjectPrincipal.class::isInstance) + .map(OidcSubjectPrincipal.class::cast) + .collect(toList()); + + if (allSubs.isEmpty()) { + return Result.failure("No 'sub' claims"); + } + + Map allIssuers = principals.stream() + .filter(OAuthProviderPrincipal.class::isInstance) + .map(OAuthProviderPrincipal.class::cast) + .collect(toMap(OAuthProviderPrincipal::getName, + OAuthProviderPrincipal::getIssuer)); + + var identities = allSubs.stream() + .map(s -> toIdentity(s, allIssuers)) + .flatMap(Optional::stream) + .collect(toList()); + + return identities.isEmpty() + ? Result.failure("No mappable 'sub' claim") + : Result.success(identities); + } + + private String buildFailureMessage(List,String>> results) { + return results.stream() + .filter(Result::isFailure) + .map(Result::getFailure) + .map(Optional::orElseThrow) + .collect(Collectors.joining(", ")); + } + + @Override + public void map(Set principals) throws AuthenticationException { + + var identities = buildIdentityList(principals); + + var lookups = identities.orElseThrow(AuthenticationException::new).stream() + .map(lookupAgent::lookup) + .collect(toList()); + + if (lookups.stream().allMatch(Result::isFailure)) { + throw new AuthenticationException("Lookup failed: " + + buildFailureMessage(lookups)); + } + + if (lookups.stream().anyMatch(Result::isFailure) && LOGGER.isDebugEnabled()) { + LOGGER.debug("Mapping succeeded with some failures: {}", + buildFailureMessage(lookups)); + } + + lookups.stream() + .map(Result::getSuccess) + .filter(Optional::isPresent) + .flatMap(o -> o.orElseThrow().stream()) + .forEach(principals::add); + } + + @Override + public void stop() throws Exception { + lookupAgent.shutdown(); + } +} diff --git a/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/CachingLookupAgent.java b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/CachingLookupAgent.java new file mode 100644 index 00000000000..7532705f470 --- /dev/null +++ b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/CachingLookupAgent.java @@ -0,0 +1,88 @@ +/* + * dCache - http://www.dcache.org/ + * + * Copyright (C) 2024 Deutsches Elektronen-Synchrotron + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.dcache.gplazma.alise; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListenableFutureTask; +import java.security.Principal; +import java.util.Collection; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import org.dcache.util.BoundedCachedExecutor; +import org.dcache.util.Result; + +import static java.util.Objects.requireNonNull; + +/** + * A simple caching layer to prevents dCache from hammering ALISE, if there are + * repeated requests for the same identity. + * + * The primary use-case is when there are multiple clients using dCache with + * different (distinct) OIDC tokens but that have the same underlying identity + * ('iss' and 'sub' claim). The dCache doors will forward each OIDC token to + * gPlazma as the bearer tokens are distinct, but repeated calls to ALISE for + * the same identity makes no sense. + */ +public class CachingLookupAgent implements LookupAgent { + + private final LookupAgent inner; + private final ExecutorService executor = new BoundedCachedExecutor(5); + + private final LoadingCache, String>> lookupResults = CacheBuilder.newBuilder() + .maximumSize(1_000) + .refreshAfterWrite(5, TimeUnit.SECONDS) + .expireAfterWrite(60, TimeUnit.SECONDS) + .build(new CacheLoader, String>>() { + @Override + public Result, String> load(Identity identity) { + return inner.lookup(identity); + } + + @Override + public ListenableFuture, String>> reload(Identity identity, Result,String> prevResult) { + var task = ListenableFutureTask.create(() -> inner.lookup(identity)); + executor.execute(task); + return task; + } + }); + + public CachingLookupAgent(LookupAgent inner) { + this.inner = requireNonNull(inner); + } + + @Override + public Result, String> lookup(Identity identity) { + try { + return lookupResults.get(identity); + } catch (ExecutionException e) { + Throwable reported = e.getCause() == null ? e : e.getCause(); + return Result.failure("Cache lookup failed: " + reported); + } + } + + @Override + public void shutdown() { + executor.shutdown(); + inner.shutdown(); + } +} diff --git a/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/Identity.java b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/Identity.java new file mode 100644 index 00000000000..f4427e696cc --- /dev/null +++ b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/Identity.java @@ -0,0 +1,63 @@ +/* + * dCache - http://www.dcache.org/ + * + * Copyright (C) 2024 Deutsches Elektronen-Synchrotron + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.dcache.gplazma.alise; + +import com.google.common.base.Objects; +import java.net.URI; +import static java.util.Objects.requireNonNull; + +/** + * The identity that ALISE will look up. + */ +public class Identity { + private final String sub; + private final URI issuer; + + public Identity(URI issuer, String sub) { + this.issuer = requireNonNull(issuer); + this.sub = requireNonNull(sub); + } + + public String sub() { + return sub; + } + + public URI issuer() { + return issuer; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Identity)) { + return false; + } + + Identity otherIdentity = (Identity)other; + return otherIdentity.sub.equals(sub) && otherIdentity.issuer.equals(issuer); + } + + @Override + public int hashCode() { + return Objects.hashCode(sub, issuer); + } +} diff --git a/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/LookupAgent.java b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/LookupAgent.java new file mode 100644 index 00000000000..488814b5847 --- /dev/null +++ b/modules/gplazma2-alise/src/main/java/org/dcache/gplazma/alise/LookupAgent.java @@ -0,0 +1,47 @@ +/* + * dCache - http://www.dcache.org/ + * + * Copyright (C) 2024 Deutsches Elektronen-Synchrotron + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.dcache.gplazma.alise; + +import java.net.URI; +import java.security.Principal; +import java.util.Collection; +import org.dcache.util.Result; + +/** + * A service that allows looking up principals (local identity) from the user's + * an OIDC subject. + */ +@FunctionalInterface +public interface LookupAgent { + /** + * Look up the principals associated with an OIDC subject from some + * trusted issuer. The result is either a corresponding set of principals + * or an error message. + * @param identity the federated identity. + * @return The result of the lookup; if successful a collection of + * principals that identify the user, if failure a suitable error message. + */ + public Result,String> lookup(Identity identity); + + /** + * A method called when the plugin is shutting down. Should release + * established resources (e.g., shutting down threads). + */ + default public void shutdown() {} +} diff --git a/modules/gplazma2-alise/src/test/java/org/dcache/gplazma/alise/AliseLookupAgentTest.java b/modules/gplazma2-alise/src/test/java/org/dcache/gplazma/alise/AliseLookupAgentTest.java new file mode 100644 index 00000000000..fe333876262 --- /dev/null +++ b/modules/gplazma2-alise/src/test/java/org/dcache/gplazma/alise/AliseLookupAgentTest.java @@ -0,0 +1,363 @@ +/* + * dCache - http://www.dcache.org/ + * + * Copyright (C) 2024 Deutsches Elektronen-Synchrotron + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.dcache.gplazma.alise; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.dcache.auth.FullNamePrincipal; +import org.dcache.auth.UserNamePrincipal; +import org.dcache.util.Result; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static com.github.npathai.hamcrestopt.OptionalMatchers.*; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class AliseLookupAgentTest { + + private HttpClient client; + private AliseLookupAgent agent; + private Identity identity; + private HttpRequest request; + private Result, String> result; + + @Before + public void setup() { + client = null; + agent = null; + identity = null; + request = null; + result = null; + } + + @Test + public void testGoodResponseNoDisplayName() throws Exception { + given(anHttpClient().thatRespondsWith(aResponse() + .withHeaders(someHeaders().withHeader("Content-Type", "application/json")) + .withBody("{\"internal\": {\"username\": \"pmillar\"}}") + .withStatusCode(200))); + given(anAliseLookupAgent() + .withApiKey("APIKEY") + .withEndpoint("https://alise.example.org/") + .withTarget("vega-kc")); + given(anIdentity().withSub("paul").withIssuer("https://issuer.example.org/")); + + whenAgentCalledWithIdentity(); + + var expected = URI.create("https://alise.example.org/api/v1" + + "/target/vega-kc" + + "/mapping/issuer/95d86dadf13ccfbd25f13d5d15edb31b4b30c971" + + "/user/paul?apikey=APIKEY"); + assertThat(request.uri(), is(equalTo(expected))); + assertThat(request.method(), is(equalTo("GET"))); + + assertThat(result.getSuccess(), isPresentAnd(contains( + new UserNamePrincipal("pmillar")))); + } + + @Test + public void testGoodResponseWithDisplayName() throws Exception { + given(anHttpClient().thatRespondsWith(aResponse() + .withHeaders(someHeaders().withHeader("Content-Type", "application/json")) + .withBody("{\"internal\": {\"username\": \"pmillar\", \"display_name\": \"Paul Millar\"}}") + .withStatusCode(200))); + given(anAliseLookupAgent() + .withApiKey("APIKEY") + .withEndpoint("https://alise.example.org/") + .withTarget("vega-kc")); + given(anIdentity().withSub("paul").withIssuer("https://issuer.example.org/")); + + whenAgentCalledWithIdentity(); + + var expected = URI.create("https://alise.example.org/api/v1" + + "/target/vega-kc" + + "/mapping/issuer/95d86dadf13ccfbd25f13d5d15edb31b4b30c971" + + "/user/paul?apikey=APIKEY"); + assertThat(request.uri(), is(equalTo(expected))); + assertThat(request.method(), is(equalTo("GET"))); + + assertThat(result.getSuccess(), isPresentAnd(containsInAnyOrder( + new UserNamePrincipal("pmillar"), + new FullNamePrincipal("Paul Millar")))); + } + + @Test + public void testMalformedResponse() throws Exception { + given(anHttpClient().thatRespondsWith(aResponse() + .withHeaders(someHeaders().withHeader("Content-Type", "application/json")) + .withBody("{}") + .withStatusCode(200))); + given(anAliseLookupAgent() + .withApiKey("APIKEY") + .withEndpoint("https://alise.example.org/") + .withTarget("vega-kc")); + given(anIdentity().withSub("paul").withIssuer("https://issuer.example.org/")); + + whenAgentCalledWithIdentity(); + + var expected = URI.create("https://alise.example.org/api/v1" + + "/target/vega-kc" + + "/mapping/issuer/95d86dadf13ccfbd25f13d5d15edb31b4b30c971" + + "/user/paul?apikey=APIKEY"); + assertThat(request.uri(), is(equalTo(expected))); + assertThat(request.method(), is(equalTo("GET"))); + + assertThat(result.getFailure(), isPresentAnd(containsString("internal"))); + } + + @Test + public void testMessageErrorResponse() throws Exception { + given(anHttpClient().thatRespondsWith(aResponse() + .withHeaders(someHeaders().withHeader("Content-Type", "application/json")) + .withBody("{\"message\": \"FNORD\"}") + .withStatusCode(400))); + given(anAliseLookupAgent() + .withApiKey("APIKEY") + .withEndpoint("https://alise.example.org/") + .withTarget("vega-kc")); + given(anIdentity().withSub("paul").withIssuer("https://issuer.example.org/")); + + whenAgentCalledWithIdentity(); + + var expected = URI.create("https://alise.example.org/api/v1" + + "/target/vega-kc" + + "/mapping/issuer/95d86dadf13ccfbd25f13d5d15edb31b4b30c971" + + "/user/paul?apikey=APIKEY"); + assertThat(request.uri(), is(equalTo(expected))); + assertThat(request.method(), is(equalTo("GET"))); + + assertThat(result.getFailure(), isPresentAnd(containsString("FNORD"))); + } + + @Test + public void testDetailErrorResponse() throws Exception { + given(anHttpClient().thatRespondsWith(aResponse() + .withHeaders(someHeaders().withHeader("Content-Type", "application/json")) + .withBody("{\"detail\": [{\"msg\": \"FOO\", \"loc\": [\"item-1\", \"item-2\"]}]}") + .withStatusCode(400))); + given(anAliseLookupAgent() + .withApiKey("APIKEY") + .withEndpoint("https://alise.example.org/") + .withTarget("vega-kc")); + given(anIdentity().withSub("paul").withIssuer("https://issuer.example.org/")); + + whenAgentCalledWithIdentity(); + + var expected = URI.create("https://alise.example.org/api/v1" + + "/target/vega-kc" + + "/mapping/issuer/95d86dadf13ccfbd25f13d5d15edb31b4b30c971" + + "/user/paul?apikey=APIKEY"); + assertThat(request.uri(), is(equalTo(expected))); + assertThat(request.method(), is(equalTo("GET"))); + + assertTrue(result.isFailure()); + String failureMessage = result.getFailure().get(); + assertThat(failureMessage, containsString("FOO")); + assertThat(failureMessage, containsString("item-1")); + assertThat(failureMessage, containsString("item-2")); + } + + private void whenAgentCalledWithIdentity() { + checkState(identity != null); + result = agent.lookup(identity); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + try { + verify(client).send(requestCaptor.capture(), any()); + } catch (InterruptedException | IOException e) { + throw new RuntimeException("verify unexpectedly throw an exception", e); + } + + request = requestCaptor.getValue(); + } + + public AliseLookupAgentBuilder anAliseLookupAgent() { + return new AliseLookupAgentBuilder(); + } + + public HttpClientBuilder anHttpClient() { + return new HttpClientBuilder(); + } + + public HttpResponseBuilder aResponse() { + return new HttpResponseBuilder(); + } + + public HttpHeadersBuilder someHeaders() { + return new HttpHeadersBuilder(); + } + + public IdentityBuilder anIdentity() { + return new IdentityBuilder(); + } + + public void given(HttpClientBuilder builder) { + client = builder.build(); + } + + public void given(AliseLookupAgentBuilder builder) { + agent = builder.build(); + } + + public void given(IdentityBuilder builder) { + identity = builder.build(); + } + + public static class HttpClientBuilder { + private final HttpClient client = mock(HttpClient.class); + private HttpResponse response; + + public HttpClientBuilder thatRespondsWith(HttpResponseBuilder builder) { + checkState(response==null); + response = builder.build(); + return this; + } + + public HttpClient build() { + try { + when(client.send(any(), any())).thenReturn(response); + } catch (InterruptedException | IOException e) { + throw new RuntimeException("Mocking generated exception", e); + } + return client; + } + } + + public static class HttpResponseBuilder { + private final HttpResponse response = mock(HttpResponse.class); + private int statusCode; + private HttpHeaders headers; + private String body; + + public HttpResponseBuilder withHeaders(HttpHeadersBuilder builder) { + checkState(headers==null); + headers = builder.build(); + return this; + } + + public HttpResponseBuilder withStatusCode(int code) { + checkArgument(code > 0); + checkState(statusCode == 0); + statusCode = code; + return this; + } + + public HttpResponseBuilder withBody(String value) { + checkState(body == null); + body = requireNonNull(value); + return this; + } + + public HttpResponse build() { + if (headers != null) { + when(response.headers()).thenReturn(headers); + } + if (statusCode != 0) { + when(response.statusCode()).thenReturn(statusCode); + } + if (body != null) { + when(response.body()).thenReturn(body); + } + return response; + } + } + + public static class HttpHeadersBuilder { + private final Map> headers = new HashMap<>(); + + public HttpHeadersBuilder withHeader(String key, String value) { + headers.computeIfAbsent(key, k -> new ArrayList()) + .add(value); + return this; + } + + public HttpHeaders build() { + return HttpHeaders.of(headers, (k,v) -> true); + } + } + + public class AliseLookupAgentBuilder { + private String apikey; + private String timeout = "PT1M"; + private URI endpoint; + private String target; + + public AliseLookupAgentBuilder withApiKey(String key) { + apikey = requireNonNull(key); + return this; + } + + public AliseLookupAgentBuilder withTimeout(String timeout) { + this.timeout = requireNonNull(timeout); + return this; + } + + public AliseLookupAgentBuilder withEndpoint(String endpoint) { + this.endpoint = URI.create(requireNonNull(endpoint)); + return this; + } + + public AliseLookupAgentBuilder withTarget(String target) { + this.target = requireNonNull(target); + return this; + } + + public AliseLookupAgent build() { + return new AliseLookupAgent(requireNonNull(client), endpoint, + target, requireNonNull(apikey), timeout); + } + } + + public static class IdentityBuilder { + private String subject; + private URI issuer; + + public IdentityBuilder withIssuer(String issuer) { + this.issuer = URI.create(requireNonNull(issuer)); + return this; + } + + public IdentityBuilder withSub(String sub) { + subject = requireNonNull(sub); + return this; + } + + public Identity build() { + return new Identity(requireNonNull(issuer), requireNonNull(subject)); + } + } +} diff --git a/modules/gplazma2-alise/src/test/java/org/dcache/gplazma/alise/AlisePluginTest.java b/modules/gplazma2-alise/src/test/java/org/dcache/gplazma/alise/AlisePluginTest.java new file mode 100644 index 00000000000..7eebf262981 --- /dev/null +++ b/modules/gplazma2-alise/src/test/java/org/dcache/gplazma/alise/AlisePluginTest.java @@ -0,0 +1,262 @@ +/* + * dCache - http://www.dcache.org/ + * + * Copyright (C) 2024 Deutsches Elektronen-Synchrotron + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.dcache.gplazma.alise; + +import java.net.URI; +import java.security.Principal; +import java.util.Collection; +import java.util.Properties; +import java.util.Set; +import org.dcache.auth.UserNamePrincipal; +import org.dcache.gplazma.AuthenticationException; +import org.dcache.util.PrincipalSetMaker; +import org.dcache.util.Result; +import org.junit.Test; +import org.junit.Before; +import org.mockito.ArgumentCaptor; +import java.util.HashSet; +import java.util.List; +import org.dcache.auth.FullNamePrincipal; + +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AlisePluginTest { + + private AlisePlugin plugin; + private Set principals; + private LookupAgent agent; + private List lookupCalls; + + @Before + public void setup() { + plugin = null; + principals = null; + agent = null; + lookupCalls = null; + } + + @Test(expected=AuthenticationException.class) + public void shouldFailIfNoSubClaim() throws Exception { + given(anAlisePlugin() + .withProprety("gplazma.alise.issuers", "") + .withAgent(aLookupAgent().thatFailsTestIfCalled())); + + whenPluginMapWith(principals().withDn("/O=ACME/CN=Wile E Coyote")); + } + + @Test(expected=AuthenticationException.class) + public void shouldFailIfMissingOpPrincipal() throws Exception { + given(anAlisePlugin() + .withProprety("gplazma.alise.issuers", "") + .withAgent(aLookupAgent().thatFailsTestIfCalled())); + + whenPluginMapWith(principals().withOidc("paul", "EXAMPLE-OP")); + } + + @Test(expected=AuthenticationException.class) + public void shouldFailIfOpNotListedByAlias() throws Exception { + given(anAlisePlugin() + .withProprety("gplazma.alise.issuers", "EXAMPLE-OP") + .withAgent(aLookupAgent().thatFailsTestIfCalled())); + + whenPluginMapWith(principals().withOidc("paul", "SOME-OTHER-OP")); + } + + @Test(expected=AuthenticationException.class) + public void shouldFailIfOpNotListedByUri() throws Exception { + given(anAlisePlugin() + .withProprety("gplazma.alise.issuers", "https://issuer.example.org/") + .withAgent(aLookupAgent().thatFailsTestIfCalled())); + + whenPluginMapWith(principals().withOidc("paul", "SOME-OTHER-OP")); + } + + @Test + public void shouldAcceptSuccessfulLookupWithUsername() throws Exception { + given(anAlisePlugin() + .withProprety("gplazma.alise.issuers", "") + .withAgent(aLookupAgent().thatReturnsSuccess(principals().withUsername("paul")))); + + whenPluginMapWith(principals() + .withOidc("paul", "EXAMPLE-OP") + .withOauth2Provider("EXAMPLE-OP", URI.create("https://issuer.example.org/"))); + + assertThat(lookupCalls, contains(new Identity(URI.create("https://issuer.example.org/"), "paul"))); + assertThat(principals, hasItem(new UserNamePrincipal("paul"))); + } + + @Test(expected=AuthenticationException.class) + public void shouldFailIfLookupFails() throws Exception { + given(anAlisePlugin() + .withProprety("gplazma.alise.issuers", "") + .withAgent(aLookupAgent().thatReturnsFailure("fnord"))); + + try { + whenPluginMapWith(principals() + .withOidc("paul", "EXAMPLE-OP") + .withOauth2Provider("EXAMPLE-OP", URI.create("https://issuer.example.org/"))); + } catch (AuthenticationException e) { + assertThat(e.getMessage(), containsString("fnord")); + throw e; + } + } + + @Test + public void shouldAcceptSuccessfulLookupWithUsernameAndFullname() throws Exception { + given(anAlisePlugin() + .withProprety("gplazma.alise.issuers", "") + .withAgent(aLookupAgent().thatReturnsSuccess(principals() + .withUsername("paul") + .withFullname("Paul Millar")))); + + whenPluginMapWith(principals() + .withOidc("paul", "EXAMPLE-OP") + .withOauth2Provider("EXAMPLE-OP", URI.create("https://issuer.example.org/"))); + + assertThat(lookupCalls, contains(new Identity(URI.create("https://issuer.example.org/"), "paul"))); + assertThat(principals, hasItems(new UserNamePrincipal("paul"), + new FullNamePrincipal("Paul Millar"))); + } + + @Test + public void shouldFilterSuppressedOPByIssuerAlias() throws Exception { + given(anAlisePlugin() + .withProprety("gplazma.alise.issuers", "EXAMPLE-OP") + .withAgent(aLookupAgent().thatReturnsSuccess(principals() + .withUsername("paul")))); + + whenPluginMapWith(principals() + .withOidc("paul", "EXAMPLE-OP") + .withOauth2Provider("EXAMPLE-OP", URI.create("https://issuer.example.org/")) + .withOidc("paul", "SOME-OTHER-OP") + .withOauth2Provider("SOME-OTHER-OP", URI.create("https://some-other-issuer.example.com/"))); + + assertThat(lookupCalls, contains(new Identity(URI.create("https://issuer.example.org/"), "paul"))); + assertThat(principals, hasItems(new UserNamePrincipal("paul"))); + } + + @Test + public void shouldFilterSuppressedOPByIssuerUri() throws Exception { + given(anAlisePlugin() + .withProprety("gplazma.alise.issuers", "https://issuer.example.org/") + .withAgent(aLookupAgent().thatReturnsSuccess(principals() + .withUsername("paul")))); + + whenPluginMapWith(principals() + .withOidc("paul", "EXAMPLE-OP") + .withOauth2Provider("EXAMPLE-OP", URI.create("https://issuer.example.org/")) + .withOidc("paul", "SOME-OTHER-OP") + .withOauth2Provider("SOME-OTHER-OP", URI.create("https://some-other-issuer.example.com/"))); + + assertThat(lookupCalls, contains(new Identity(URI.create("https://issuer.example.org/"), "paul"))); + assertThat(principals, hasItems(new UserNamePrincipal("paul"))); + } + + private AlisePluginBuilder anAlisePlugin() { + return new AlisePluginBuilder(); + } + + private LookupAgentBuilder aLookupAgent() { + return new LookupAgentBuilder(); + } + + private PrincipalSetMaker principals() { + return new PrincipalSetMaker(); + } + + private void given(AlisePluginBuilder builder) { + plugin = builder.build(); + } + + private void whenPluginMapWith(PrincipalSetMaker maker) throws AuthenticationException { + // PrincipalSetMaker creates an unmodifiable Set; but, for gPlazma, we need a Set that can be modified. + principals = new HashSet<>(maker.build()); + + plugin.map(principals); + + ArgumentCaptor identityCaptor = ArgumentCaptor.forClass(Identity.class); + verify(agent).lookup(identityCaptor.capture()); + lookupCalls = identityCaptor.getAllValues(); + } + + private class AlisePluginBuilder { + private final Properties config = new Properties(); + + private AlisePluginBuilder withProprety(String key, String value) { + config.setProperty(key, value); + return this; + } + + private AlisePluginBuilder withAgent(LookupAgentBuilder builder) { + agent = builder.build(); + return this; + } + + private AlisePlugin build() { + checkState(agent != null); + return new AlisePlugin(config, agent); + } + } + + private static class LookupAgentBuilder { + private final LookupAgent agent = mock(LookupAgent.class); + private Result,String> result; + private boolean failTest; + + private LookupAgentBuilder thatReturnsFailure(String failure) { + checkState(result == null); + checkState(!failTest); + result = Result.failure(requireNonNull(failure)); + return this; + } + + private LookupAgentBuilder thatReturnsSuccess(PrincipalSetMaker maker) { + checkState(result == null); + checkState(!failTest); + result = Result.success(maker.build()); + return this; + } + + private LookupAgentBuilder thatFailsTestIfCalled() { + checkState(result == null); + failTest = true; + return this; + } + + private LookupAgent build() { + checkState(result != null || failTest); + if (failTest) { + when(agent.lookup(any())).thenThrow(new AssertionError("Unexpected call to LookupAgent#lookup")); + } else { + when(agent.lookup(any())).thenReturn(result); + } + return agent; + } + } +} diff --git a/packages/pom.xml b/packages/pom.xml index a8fff140fe0..7caee8b0ee1 100644 --- a/packages/pom.xml +++ b/packages/pom.xml @@ -105,6 +105,11 @@ missingfiles-semsg ${project.version} + + org.dcache + gplazma2-alise + ${project.version} + org.dcache gplazma2-argus diff --git a/pom.xml b/pom.xml index 5d871f617e3..49ee761b13d 100644 --- a/pom.xml +++ b/pom.xml @@ -1467,6 +1467,7 @@ modules/common-security modules/cells modules/gplazma2 + modules/gplazma2-alise modules/gplazma2-argus modules/gplazma2-fermi modules/gplazma2-grid diff --git a/skel/share/defaults/gplazma.properties b/skel/share/defaults/gplazma.properties index c6835c5ca64..af1ca675e4c 100644 --- a/skel/share/defaults/gplazma.properties +++ b/skel/share/defaults/gplazma.properties @@ -523,11 +523,11 @@ gplazma.oidc.audience-targets = # gplazma.oidc-te.url = ${gplazma.oidc.provider} -gplazma.oidc-te.client-id = token-exchange +gplazma.oidc-te.client-id = token-exchange gplazma.oidc-te.client-secret = xxx gplazma.oidc-te.grant-type = urn:ietf:params:oauth:grant-type:token-exchange gplazma.oidc-te.subject-issuer = oidc -gplazma.oidc-te.subject-token-type = urn:ietf:params:oauth:token-type:access_token +gplazma.oidc-te.subject-token-type = urn:ietf:params:oauth:token-type:access_token gplazma.oidc-te.audience = token-exchange @@ -656,6 +656,38 @@ gplazma.roles.observer-gid = # gplazma.roles.qos-gid= + +# ---- ALISE plugin +# +# The ALISE plugin provides support in dCache to query an ALISE service +# when converting a user's federated identity (as asserted by some OIDC +# access token) to the site-local identity (given by a username). +# +# The endpoint of the ALISE service; e.g., https://alise.example.org/. This +# configuration property is required. +gplazma.alise.endpoint = + +# The API KEY used to authorise dCache's access to ALISE. This configuration +# property is required. +gplazma.alise.apikey = + +# The ALISE-internal name for the set of identities to use; required. This +# configuration property is required. +gplazma.alise.target = + +# The timeout when making a request, using ISO-8601 notation for a duration. +# This configuration property is required, but may be left with the default +# value. +gplazma.alise.timeout = PT10S + +# A space-separated list of issuers. If this list is non-empty then only +# identities asserted by an OP on this list are looked up within ALISE, +# identities asserted by other OPs are not sent. The list entries may be +# either the issuer's alises (dCache-internal names for OPs) or the issuer's +# URI. If the list is empty then all OIDC identities are sent to ALISE. +gplazma.alise.issuers = + + # # # -----------------------------------------------------------------------