Skip to content

Commit

Permalink
gplazma alise initial version of plugin
Browse files Browse the repository at this point in the history
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
  • Loading branch information
paulmillar committed Aug 1, 2024
1 parent fd24530 commit e0a93e7
Show file tree
Hide file tree
Showing 13 changed files with 1,370 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down
64 changes: 64 additions & 0 deletions modules/gplazma2-alise/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.dcache</groupId>
<artifactId>dcache-parent</artifactId>
<version>10.2.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>gplazma2-alise</artifactId>
<packaging>jar</packaging>

<name>gPlazma 2 ALISE plugin</name>

<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.dcache</groupId>
<artifactId>dcache-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.dcache</groupId>
<artifactId>gplazma2</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.npathai</groupId>
<artifactId>hamcrest-optional</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Collection<Principal>,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<Collection<Principal>, String> resultFromResponse(HttpResponse<String> response) {
Optional<String> 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<Collection<Principal>, 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<Principal> 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);
}
}
Loading

0 comments on commit e0a93e7

Please sign in to comment.