Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(agama): add utility classes for inbound identity #2231

Merged
merged 2 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions agama/inboundID/CustomMappings.java.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ public final class CustomMappings {

profile -> {
Map<String, Object> map = new HashMap<>();
//Fill your map as desired with data from input profile. See examples in class io.jans.inbound.Mappings
//Fill your map as desired with data from input profile. See examples in class io.jans.inbound.Mappings:
//https://github.com/JanssenProject/jans/blob/main/agama/inboundID/src/main/java/io/jans/inbound/Mappings.java

//value = profile.get("...")
//map.put(Attrs.UID, ... );
return map;
return map;
};

private CustomMappings() { }
Expand Down
25 changes: 24 additions & 1 deletion agama/inboundID/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,42 @@
<artifactId>jboss-jaxrs-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>

<!-- LOGGING -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<scope>provided</scope>
</dependency>

<!-- JANSSEN -->
<dependency>
<groupId>io.jans</groupId>
<artifactId>jans-core-util</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.jans</groupId>
<artifactId>jans-core-service</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jans</groupId>
<artifactId>jans-orm-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jans</groupId>
<artifactId>jans-orm-model</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jans</groupId>
<artifactId>jans-auth-common</artifactId>
<scope>provided</scope>
</dependency>

<!-- NIMBUS -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
Expand Down
129 changes: 115 additions & 14 deletions agama/inboundID/src/main/java/io/jans/inbound/IdentityProcessor.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
package io.jans.inbound;

import io.jans.as.common.model.common.User;
import io.jans.as.common.service.common.UserService;
import io.jans.orm.model.base.CustomObjectAttribute;
import io.jans.service.cdi.util.CdiUtil;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.Map;
import java.util.function.UnaryOperator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class IdentityProcessor {

private Provider provider;
private UnaryOperator<Map<String, Object>> mapping;

public IdentityProcessor(Provider provider, ClassLoader classLoader)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

public IdentityProcessor() { }

public IdentityProcessor(Provider provider) {
this.provider = provider;
this.mapping = getMapping(provider.getMappingClassField(),
classLoader == null ? getClass().getClassLoader() : classLoader);

}

public Map<String, List<Object>> applyMapping(Map<String, Object> profile) {
public Map<String, List<Object>> applyMapping(Map<String, Object> profile, ClassLoader classLoader)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

Map<String, Object> pr = mapping.apply(profile);
UnaryOperator<Map<String, Object>> op = getMapping(provider.getMappingClassField(),
classLoader == null ? getClass().getClassLoader() : classLoader);
Map<String, Object> pr = op.apply(profile);
Map<String, List<Object>> res = new HashMap<>();

for (String key: pr.keySet()) {
Expand All @@ -48,12 +56,105 @@ public Map<String, List<Object>> applyMapping(Map<String, Object> profile) {
return res;

}

public String process(Map<String, List<Object>> profile) {

if (profile.isEmpty()) throw new IllegalArgumentException("Empty profile data");

Logger logger = LoggerFactory.getLogger(getClass());
UserService userService = CdiUtil.bean(UserService.class);
logger.info("User provisioning started");

User user = null;
boolean update = true;
String uid = profile.get(Attrs.UID).get(0).toString();
String email = Optional.ofNullable(profile.get(Attrs.MAIL)).orElse(Collections.emptyList())
.stream().findFirst().map(Object::toString).orElse(null);

if (email != null && !email.contains("@")) throw new IllegalArgumentException("Invalid e-mail " + email);

Map<String, List<Object>> profile2 = new HashMap<>(profile); //ensure it is modifiable
if (provider.isEmailLinkingSafe() && email != null) {

user = userService.getUserByAttribute(Attrs.MAIL, email);
if (user != null) {
logger.debug("Identity of incoming user is matched to existing {} by e-mail linking. " +
"Ignoring incoming uid {}", user.getUserId(), uid);

uid = user.getUserId();
profile2.remove(Attrs.UID);
}
}

if (user == null) {
logger.debug("Retrieving user identified by {}", uid);
user = userService.getUser(uid);
update = user != null;
}

if (update) {

if (provider.isSkipProfileUpdate()) {
logger.info("Skipping profile update");
} else {
logger.info("Updating user {}", uid);
user.setCustomAttributes(attributesForUpdate(
user.getCustomAttributes(), profile2, provider.isCumulativeUpdate()));
userService.updateUser(user);
}

} else {
logger.info("Adding user {}", uid);

user = new User();
user.setCustomAttributes(attributesForAdd(profile));
userService.addUser(user, true);
}

return uid;

}

private List<CustomObjectAttribute> attributesForUpdate(List<CustomObjectAttribute> customAttributes,
Map<String, List<Object>> profile, boolean cumulative) {

//Merge existing data of user plus incoming data in profile
List<CustomObjectAttribute> customAttrs = new ArrayList<>(customAttributes);

for (CustomObjectAttribute coa : customAttrs) {
String attrName = coa.getName();
List<Object> newValues = profile.get(attrName);

if (newValues != null) {
List<Object> values = new ArrayList<>(cumulative ? coa.getValues() : Collections.emptyList());
newValues.stream().filter(nv -> !values.contains(nv)).forEach(values::add);

profile.remove(attrName);
coa.setValues(values);
}
}

profile.forEach((k, v) -> {
CustomObjectAttribute coa = new CustomObjectAttribute(k);
coa.setValues(v);
customAttrs.add(coa);
});

return customAttrs;

}

public String process(Map<String, List<?>> profile) {//throws Exception {
//Provisions the user and returns its local id (inum)
//reject if there are null values
if (profile.isEmpty() && provider == null) return null;
return "";
private List<CustomObjectAttribute> attributesForAdd(Map<String, List<Object>> profile) {

List<CustomObjectAttribute> attrs = new ArrayList<>();
profile.forEach((k, v) -> {
CustomObjectAttribute coa = new CustomObjectAttribute(k);
coa.setValues(v);
attrs.add(coa);
});

return attrs;

}

private UnaryOperator<Map<String, Object>> getMapping(String field, ClassLoader clsLoader)
Expand Down
73 changes: 45 additions & 28 deletions agama/inboundID/src/main/java/io/jans/inbound/Mappings.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.jans.inbound;

import java.util.function.UnaryOperator;
import java.util.HashMap;
import java.util.Map;

/**
Expand All @@ -11,45 +12,61 @@ public final class Mappings {

public static final UnaryOperator<Map<String, Object>>

GOOGLE = profile -> Map.of(
Attrs.UID, "google-" + profile.get("sub"),
Attrs.MAIL, profile.get("email"),
Attrs.CN, profile.get("name"),
Attrs.SN, profile.get("family_name"),
Attrs.DISPLAY_NAME, profile.get("given_name"),
Attrs.GIVEN_NAME, profile.get("given_name")
);
GOOGLE = profile -> {
Map<String, Object> map = new HashMap<>();

map.put(Attrs.UID, "google-" + profile.get("sub"));
map.put(Attrs.MAIL, profile.get("email"));
map.put(Attrs.CN, profile.get("name"));
map.put(Attrs.SN, profile.get("family_name"));
map.put(Attrs.DISPLAY_NAME, profile.get("given_name"));
map.put(Attrs.GIVEN_NAME, profile.get("given_name"));

return map;
};

public static final UnaryOperator<Map<String, Object>>
//See https://developers.facebook.com/docs/graph-api/reference/user

FACEBOOK = profile -> Map.of(
Attrs.UID, "facebook-" + profile.get("id"),
Attrs.MAIL, profile.get("email"),
Attrs.CN, profile.get("name"),
Attrs.SN, profile.get("last_name"),
Attrs.DISPLAY_NAME, profile.get("first_name"),
Attrs.GIVEN_NAME, profile.get("first_name")
);
FACEBOOK = profile -> {
Map<String, Object> map = new HashMap<>();

map.put(Attrs.UID, "facebook-" + profile.get("id"));
map.put(Attrs.MAIL, profile.get("email"));
map.put(Attrs.CN, profile.get("name"));
map.put(Attrs.SN, profile.get("last_name"));
map.put(Attrs.DISPLAY_NAME, profile.get("first_name"));
map.put(Attrs.GIVEN_NAME, profile.get("first_name"));

return map;
};

public static final UnaryOperator<Map<String, Object>>

APPLE = profile -> Map.of(
Attrs.UID, "apple-" + profile.get("sub"),
Attrs.MAIL, profile.get("email"),
Attrs.DISPLAY_NAME, profile.get("name"),
Attrs.GIVEN_NAME, profile.get("name")
);
APPLE = profile -> {
Map<String, Object> map = new HashMap<>();

map.put(Attrs.UID, "apple-" + profile.get("sub"));
map.put(Attrs.MAIL, profile.get("email"));
map.put(Attrs.DISPLAY_NAME, profile.get("name"));
map.put(Attrs.GIVEN_NAME, profile.get("name"));

return map;
};

public static final UnaryOperator<Map<String, Object>>
//See https://docs.github.com/en/rest/users/users

GITHUB = profile -> Map.of(
Attrs.UID, "github-" + profile.getOrDefault("login", profile.get("id")),
Attrs.MAIL, profile.get("email"),
Attrs.DISPLAY_NAME, profile.get("name"),
Attrs.GIVEN_NAME, profile.get("name")
);
GITHUB = profile -> {
Map<String, Object> map = new HashMap<>();

map.put(Attrs.UID, "github-" + profile.getOrDefault("login", profile.get("id")));
map.put(Attrs.MAIL, profile.get("email"));
map.put(Attrs.DISPLAY_NAME, profile.get("name"));
map.put(Attrs.GIVEN_NAME, profile.get("name"));

return map;
};

private Mappings() { }

Expand Down
9 changes: 9 additions & 0 deletions agama/inboundID/src/main/java/io/jans/inbound/Provider.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class Provider {

private boolean enabled = true;
private boolean skipProfileUpdate;
private boolean cumulativeUpdate;
private boolean requestForEmail;
private boolean emailLinkingSafe;

Expand Down Expand Up @@ -51,6 +52,14 @@ public boolean isSkipProfileUpdate() {
public void setSkipProfileUpdate(boolean skipProfileUpdate) {
this.skipProfileUpdate = skipProfileUpdate;
}

public boolean isCumulativeUpdate() {
return cumulativeUpdate;
}

public void setCumulativeUpdate(boolean cumulativeUpdate) {
this.cumulativeUpdate = cumulativeUpdate;
}

public String getLogoImg() {
return logoImg;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Collections;
Expand Down
4 changes: 3 additions & 1 deletion docs/admin/developer/agama/java-classpath.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ Any public method from a public class or static method from a public interface t

Additionally, it is possible to upload source code on the fly to augment the classpath. Any valid Java or Groovy file is accepted and must be located under `/opt/jans/jetty/jans-auth/agama/scripts`. A class named `com.acme.Person` for instance, must reside in `com/acme/Person` under the `scripts` directory.

This "hot" reloading feature is a big time saver while developing flows because there is no need to restart the jans-auth webapp. Also, only the files that get modified are effectively compiled in the background.
Specifically, classes in `scripts` directory can only be accessed through `Call` directives. As an example suppose you added classes `A` and `B` to `scripts`, and `A` depends on `B`. `Call`s using class `A` will work and any change to files `A` and/or `B` will be picked automatically. On the contrary, trying to load this kind of classes using `Class.forName` either from a jar file in `custom/libs` or from Agama itself will degenerate in `ClassNotFoundException`. Note `A` and `B` can also depend on classes found in any of the three locations listed above.

This "hot" reloading feature can be a big time saver while developing flows because there is no need to restart the jans-auth webapp. Also, only the files that get modified are effectively re-compiled.
Loading