From bc49aea6589ef24ed970859232b0290047241bc6 Mon Sep 17 00:00:00 2001 From: flytreeleft Date: Fri, 26 Apr 2019 23:02:54 +0800 Subject: [PATCH] Support multiple Keycloak Realms --- .../plugin/KeycloakAuthenticatingRealm.java | 13 +- .../KeycloakAuthorizationManager.java | 18 +- .../plugin/internal/KeycloakUserManager.java | 6 +- .../plugin/internal/NexusKeycloakClient.java | 54 ++++-- .../internal/NexusKeycloakClientLoader.java | 180 ++++++++++++++++++ .../internal/mapper/KeycloakMapper.java | 35 ++-- 6 files changed, 250 insertions(+), 56 deletions(-) create mode 100644 src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/NexusKeycloakClientLoader.java diff --git a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/KeycloakAuthenticatingRealm.java b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/KeycloakAuthenticatingRealm.java index 536315b..d77da44 100755 --- a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/KeycloakAuthenticatingRealm.java +++ b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/KeycloakAuthenticatingRealm.java @@ -12,6 +12,9 @@ */ package org.github.flytreeleft.nexus3.keycloak.plugin; +import javax.inject.Named; +import javax.inject.Singleton; + import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; @@ -24,13 +27,10 @@ import org.apache.shiro.subject.PrincipalCollection; import org.eclipse.sisu.Description; import org.github.flytreeleft.nexus3.keycloak.plugin.internal.NexusKeycloakClient; +import org.github.flytreeleft.nexus3.keycloak.plugin.internal.NexusKeycloakClientLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - @Singleton @Named @Description("Keycloak Authentication Realm") @@ -40,9 +40,8 @@ public class KeycloakAuthenticatingRealm extends AuthorizingRealm { private NexusKeycloakClient client; - @Inject - public KeycloakAuthenticatingRealm(final NexusKeycloakClient client) { - this.client = client; + public KeycloakAuthenticatingRealm() { + this.client = NexusKeycloakClientLoader.loadClient(); } @Override diff --git a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/KeycloakAuthorizationManager.java b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/KeycloakAuthorizationManager.java index 4800756..8564d05 100755 --- a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/KeycloakAuthorizationManager.java +++ b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/KeycloakAuthorizationManager.java @@ -12,6 +12,12 @@ */ package org.github.flytreeleft.nexus3.keycloak.plugin.internal; +import java.util.Collections; +import java.util.Set; +import javax.enterprise.inject.Typed; +import javax.inject.Named; +import javax.inject.Singleton; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonatype.nexus.security.authz.AbstractReadOnlyAuthorizationManager; @@ -21,13 +27,6 @@ import org.sonatype.nexus.security.role.NoSuchRoleException; import org.sonatype.nexus.security.role.Role; -import javax.enterprise.inject.Typed; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import java.util.Collections; -import java.util.Set; - @Singleton @Typed(AuthorizationManager.class) @Named("Keycloak") @@ -36,10 +35,9 @@ public class KeycloakAuthorizationManager extends AbstractReadOnlyAuthorizationM private NexusKeycloakClient client; - @Inject - public KeycloakAuthorizationManager(NexusKeycloakClient client) { + public KeycloakAuthorizationManager() { LOGGER.info("KeycloakAuthorizationManager is starting..."); - this.client = client; + this.client = NexusKeycloakClientLoader.loadClient(); } @Override diff --git a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/KeycloakUserManager.java b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/KeycloakUserManager.java index 7a3d619..992bec6 100755 --- a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/KeycloakUserManager.java +++ b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/KeycloakUserManager.java @@ -18,7 +18,6 @@ import javax.inject.Named; import javax.inject.Singleton; -import com.google.inject.Inject; import org.github.flytreeleft.nexus3.keycloak.plugin.KeycloakAuthenticatingRealm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,10 +37,9 @@ public class KeycloakUserManager extends AbstractReadOnlyUserManager { private NexusKeycloakClient client; - @Inject - public KeycloakUserManager(NexusKeycloakClient client) { + public KeycloakUserManager() { LOGGER.info("KeycloakUserManager is starting..."); - this.client = client; + this.client = NexusKeycloakClientLoader.loadClient(); } @Override diff --git a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/NexusKeycloakClient.java b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/NexusKeycloakClient.java index 104b8cf..2973c00 100644 --- a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/NexusKeycloakClient.java +++ b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/NexusKeycloakClient.java @@ -3,12 +3,10 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import javax.inject.Named; -import javax.inject.Singleton; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.util.StringUtils; @@ -23,14 +21,22 @@ import org.sonatype.nexus.security.user.User; import org.sonatype.nexus.security.user.UserSearchCriteria; -@Singleton -@Named("NexusKeycloakClient") public class NexusKeycloakClient { - private static final String CONFIG_FILE = "keycloak.json"; private static final Logger LOGGER = LoggerFactory.getLogger(NexusKeycloakClient.class); + private Path config; + private boolean roleWithRealm; private transient KeycloakAdminClient keycloakAdminClient; + public NexusKeycloakClient(Path config) { + this(config, false); + } + + public NexusKeycloakClient(Path config, boolean roleWithRealm) { + this.config = config; + this.roleWithRealm = roleWithRealm; + } + public boolean authenticate(UsernamePasswordToken token) { AccessTokenResponse accessTokenResponse = getKeycloakAdminClient().obtainAccessToken(token.getUsername(), new String(token.getPassword())); @@ -47,7 +53,7 @@ public Set findRoleIdsByUserId(String userId) { List realmGroups = getKeycloakAdminClient().getRealmGroupsOfUser(user); // Convert to compatible roles to make sure the existing role-mappings are still working - return KeycloakMapper.toCompatibleRoleIds(clientRoles, realmRoles, realmGroups); + return KeycloakMapper.toCompatibleRoleIds(getRoleRealm(), clientRoles, realmRoles, realmGroups); } public User findUserByUserId(String userId) { @@ -57,24 +63,27 @@ public User findUserByUserId(String userId) { } public Role findRoleByRoleId(String roleId) { - RoleRepresentation role; + String[] splits = roleId.split(":"); + String roleType = splits.length > 1 ? splits[0] : null; + String roleRealm = splits.length > 2 ? splits[1] : null; + String roleName = splits[splits.length - 1]; - if (roleId.startsWith(KeycloakMapper.REALM_GROUP_PREFIX)) { - String groupPath = roleId.substring(KeycloakMapper.REALM_GROUP_PREFIX.length() + 1); - GroupRepresentation group = getKeycloakAdminClient().getRealmGroupByGroupPath(groupPath); + if (roleRealm != null && !getKeycloakAdminClient().getConfig().getRealm().equals(roleRealm)) { + return null; + } + + RoleRepresentation role; + if (KeycloakMapper.REALM_GROUP_PREFIX.equals(roleType)) { + GroupRepresentation group = getKeycloakAdminClient().getRealmGroupByGroupPath(roleName); - return KeycloakMapper.toRole(group); - } else if (roleId.startsWith(KeycloakMapper.REALM_ROLE_PREFIX)) { - String roleName = roleId.substring(KeycloakMapper.REALM_ROLE_PREFIX.length() + 1); + return KeycloakMapper.toRole(getRoleRealm(), group); + } else if (KeycloakMapper.REALM_ROLE_PREFIX.equals(roleType)) { role = getKeycloakAdminClient().getRealmRoleByRoleName(roleName); } else { - String roleName = roleId.startsWith(KeycloakMapper.CLIENT_ROLE_PREFIX) - ? roleId.substring(KeycloakMapper.CLIENT_ROLE_PREFIX.length() + 1) - : roleId; String client = getKeycloakAdminClient().getConfig().getResource(); role = getKeycloakAdminClient().getRealmClientRoleByRoleName(client, roleName); } - return KeycloakMapper.toRole(role); + return KeycloakMapper.toRole(getRoleRealm(), role); } public Set findAllUserIds() { @@ -109,7 +118,12 @@ public Set findRoles() { List realmRoles = getKeycloakAdminClient().getRealmRoles(); List realmGroups = getKeycloakAdminClient().getRealmGroups(); - return KeycloakMapper.toRoles(clientRoles, realmRoles, realmGroups); + return KeycloakMapper.toRoles(getRoleRealm(), clientRoles, realmRoles, realmGroups); + } + + protected String getRoleRealm() { + String realm = getKeycloakAdminClient().getConfig().getRealm(); + return this.roleWithRealm ? realm : null; } private synchronized KeycloakAdminClient getKeycloakAdminClient() { @@ -124,6 +138,6 @@ private synchronized KeycloakAdminClient getKeycloakAdminClient() { } private InputStream getKeycloakJson() throws IOException { - return Files.newInputStream(Paths.get(".", "etc", CONFIG_FILE)); + return Files.newInputStream(this.config); } } diff --git a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/NexusKeycloakClientLoader.java b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/NexusKeycloakClientLoader.java new file mode 100644 index 0000000..906f8b3 --- /dev/null +++ b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/NexusKeycloakClientLoader.java @@ -0,0 +1,180 @@ +package org.github.flytreeleft.nexus3.keycloak.plugin.internal; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.io.FileUtils; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonatype.nexus.security.role.Role; +import org.sonatype.nexus.security.user.User; +import org.sonatype.nexus.security.user.UserSearchCriteria; + +public class NexusKeycloakClientLoader { + private static final String PLUGIN_CONFIG_FILE = "keycloak-plugin.properties"; + private static final String CONFIG_FILE = "keycloak.json"; + + private static final String PLUGIN_CONFIG_KEY_KEYCLOAK_AUTH_CONFIG = "keycloak.auth.config"; + + private static final Logger LOGGER = LoggerFactory.getLogger(NexusKeycloakClientLoader.class); + + private static NexusKeycloakClient client; + + public static NexusKeycloakClient loadClient() { + if (client == null) { + Properties props = getPluginProperties(); + String keycloakJSON = props.getProperty(PLUGIN_CONFIG_KEY_KEYCLOAK_AUTH_CONFIG, "").trim(); + String[] jsonFiles = keycloakJSON.isEmpty() + ? new String[] { CONFIG_FILE } + : keycloakJSON.split("\\s*(,|;)\\s*"); + + if (jsonFiles.length == 1) { + client = new NexusKeycloakClient(Paths.get(".", "etc", jsonFiles[0])); + + LOGGER.info(String.format("Create NexusKeycloakClient with %s", jsonFiles[0])); + } else { + List clients = new ArrayList<>(); + for (String jsonFile : jsonFiles) { + NexusKeycloakClient c = new NexusKeycloakClient(Paths.get(".", "etc", jsonFile), true); + clients.add(c); + } + + client = new CompositeNexusKeycloakClient(clients); + + LOGGER.info(String.format("Create CompositeNexusKeycloakClient with %d keycloak.json files: %s", + jsonFiles.length, + keycloakJSON)); + } + } + return client; + } + + public static Properties getPluginProperties() { + Properties props = new Properties(); + + File config = FileUtils.getFile(".", "etc", PLUGIN_CONFIG_FILE); + if (config.exists()) { + try { + props.load(FileUtils.openInputStream(config)); + } catch (IOException e) { + throw new IllegalStateException("Can not read the plugin properties", e); + } + } + return props; + } + + static class CompositeNexusKeycloakClient extends NexusKeycloakClient { + private List clients; + + public CompositeNexusKeycloakClient(List clients) { + super(null); + this.clients = clients != null ? clients : new ArrayList<>(); + } + + @Override + public boolean authenticate(UsernamePasswordToken token) { + for (NexusKeycloakClient client : this.clients) { + // Do authenticate for the first matching + if (client.findUserByUserId(token.getUsername()) != null) { + return client.authenticate(token); + } + } + return false; + } + + @Override + public Set findRoleIdsByUserId(String userId) { + for (NexusKeycloakClient client : this.clients) { + if (client.findUserByUserId(userId) != null) { + return client.findRoleIdsByUserId(userId); + } + } + return new HashSet<>(); + } + + @Override + public User findUserByUserId(String userId) { + for (NexusKeycloakClient client : this.clients) { + User user = client.findUserByUserId(userId); + if (user != null) { + return user; + } + } + return null; + } + + @Override + public Role findRoleByRoleId(String roleId) { + for (NexusKeycloakClient client : this.clients) { + Role role = client.findRoleByRoleId(roleId); + if (role != null) { + return role; + } + } + return null; + } + + @Override + public Set findAllUserIds() { + Set userIds = new HashSet<>(); + + for (NexusKeycloakClient client : this.clients) { + Set set = client.findAllUserIds(); + // Remove the existing users + set.removeAll(userIds); + // Add the new users to the result + userIds.addAll(set); + } + return userIds; + } + + @Override + public Set findUsers() { + Set users = new HashSet<>(); + + for (NexusKeycloakClient client : this.clients) { + Set set = client.findUsers(); + // Remove the existing users + set.removeAll(users); + // Add the new users to the result + users.addAll(set); + } + return users; + } + + @Override + public Set findUserByCriteria(UserSearchCriteria criteria) { + Set users = new HashSet<>(); + + for (NexusKeycloakClient client : this.clients) { + Set set = client.findUserByCriteria(criteria); + // Remove the existing users + set.removeAll(users); + // Add the new users to the result + users.addAll(set); + } + return users; + } + + @Override + public Set findRoles() { + Set roles = new HashSet<>(); + + for (NexusKeycloakClient client : this.clients) { + Set set = client.findRoles(); + // Remove the existing roles + set.removeAll(roles); + // Add the new roles to the result + roles.addAll(set); + } + return roles; + } + } +} diff --git a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/mapper/KeycloakMapper.java b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/mapper/KeycloakMapper.java index f4b1c5f..034c1cb 100644 --- a/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/mapper/KeycloakMapper.java +++ b/src/main/java/org/github/flytreeleft/nexus3/keycloak/plugin/internal/mapper/KeycloakMapper.java @@ -43,14 +43,15 @@ public static Set toUsers(List representations) { return users; } - public static Role toRole(RoleRepresentation representation) { + public static Role toRole(String realm, RoleRepresentation representation) { if (representation == null) { return null; } Role role = new Role(); String prefix = representation.getClientRole() ? CLIENT_ROLE_PREFIX : REALM_ROLE_PREFIX; - String roleName = String.format("%s:%s", prefix, representation.getName()); + // [:]: + String roleName = String.format("%s%s:%s", prefix, realm != null ? ":" + realm : "", representation.getName()); // Use role name as role-id and role-name of Nexus3 role.setRoleId(roleName); @@ -64,13 +65,17 @@ public static Role toRole(RoleRepresentation representation) { return role; } - public static Role toRole(GroupRepresentation representation) { + public static Role toRole(String realm, GroupRepresentation representation) { if (representation == null) { return null; } Role role = new Role(); - String roleName = String.format("%s:%s", REALM_GROUP_PREFIX, representation.getPath()); + // [:]: + String roleName = String.format("%s%s:%s", + REALM_GROUP_PREFIX, + realm != null ? ":" + realm : "", + representation.getPath()); role.setRoleId(roleName); role.setName(roleName); @@ -80,20 +85,20 @@ public static Role toRole(GroupRepresentation representation) { return role; } - public static Set toRoles(List... lists) { - return toRoles(lists, false); + public static Set toRoles(String realm, List... lists) { + return toRoles(realm, lists, false); } - public static Set toRoleIds(List... lists) { - return toRoleIds(lists, false); + public static Set toRoleIds(String realm, List... lists) { + return toRoleIds(realm, lists, false); } /** Just for compatibility */ - public static Set toCompatibleRoleIds(List... lists) { - return toRoleIds(lists, true); + public static Set toCompatibleRoleIds(String realm, List... lists) { + return toRoleIds(realm, lists, true); } - private static Set toRoles(List[] lists, boolean forCompatible) { + private static Set toRoles(String realm, List[] lists, boolean forCompatible) { Set roles = new LinkedHashSet<>(); for (List list : lists) { @@ -107,18 +112,18 @@ private static Set toRoles(List[] lists, boolean forCompatible) { roles.add(toCompatibleRole((RoleRepresentation) representation)); } - roles.add(toRole((RoleRepresentation) representation)); + roles.add(toRole(realm, (RoleRepresentation) representation)); } else if (representation instanceof GroupRepresentation) { - roles.add(toRole((GroupRepresentation) representation)); + roles.add(toRole(realm, (GroupRepresentation) representation)); } } } return roles; } - private static Set toRoleIds(List[] lists, boolean forCompatible) { + private static Set toRoleIds(String realm, List[] lists, boolean forCompatible) { Set roleIds = new LinkedHashSet<>(); - roleIds.addAll(toRoles(lists, forCompatible).stream().map(Role::getRoleId).collect(Collectors.toList())); + roleIds.addAll(toRoles(realm, lists, forCompatible).stream().map(Role::getRoleId).collect(Collectors.toList())); return roleIds; }