com.sitewhere.microservice.user.KeycloakUserManagement Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sitewhere-microservice Show documentation
Show all versions of sitewhere-microservice Show documentation
SiteWhere Microservice Components Library
The newest version!
/**
* Copyright © 2014-2021 The SiteWhere Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sitewhere.microservice.user;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.core.Response;
import org.apache.http.HttpStatus;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.ServerInfoResource;
import org.keycloak.admin.client.token.TokenManager;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.KeysMetadataRepresentation.KeyMetadataRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.info.SystemInfoRepresentation;
import com.sitewhere.microservice.lifecycle.LifecycleComponent;
import com.sitewhere.microservice.util.MarshalUtils;
import com.sitewhere.rest.model.search.Pager;
import com.sitewhere.rest.model.search.SearchResults;
import com.sitewhere.rest.model.user.GrantedAuthority;
import com.sitewhere.rest.model.user.Role;
import com.sitewhere.rest.model.user.User;
import com.sitewhere.spi.SiteWhereException;
import com.sitewhere.spi.microservice.instance.IInstanceSettings;
import com.sitewhere.spi.microservice.lifecycle.ILifecycleProgressMonitor;
import com.sitewhere.spi.microservice.lifecycle.LifecycleComponentType;
import com.sitewhere.spi.microservice.user.IUserManagement;
import com.sitewhere.spi.search.ISearchResults;
import com.sitewhere.spi.user.IGrantedAuthority;
import com.sitewhere.spi.user.IGrantedAuthoritySearchCriteria;
import com.sitewhere.spi.user.IRole;
import com.sitewhere.spi.user.IRoleSearchCriteria;
import com.sitewhere.spi.user.IUser;
import com.sitewhere.spi.user.IUserSearchCriteria;
import com.sitewhere.spi.user.request.IGrantedAuthorityCreateRequest;
import com.sitewhere.spi.user.request.IRoleCreateRequest;
import com.sitewhere.spi.user.request.IUserCreateRequest;
import io.sitewhere.k8s.api.ISiteWhereKubernetesClient;
/**
* Implementation of {@link IUserManagement} that interacts with an underlying
* Keycloak instance.
*/
@ApplicationScoped
public class KeycloakUserManagement extends LifecycleComponent implements IUserManagement {
/** Client id for OpenID Connect support */
private static final String CLIENT_ID_OPENID_CONNECT = "sitewhere-openid";
/** Keycloak client */
private Keycloak keycloak;
/** OpenID Connect client secret */
private String clientSecret;
public KeycloakUserManagement() {
super(LifecycleComponentType.DataStore);
}
/**
* Get server URL for processing requests.
*
* @return
*/
protected String getServerUrl() {
IInstanceSettings settings = getMicroservice().getInstanceSettings();
String serviceName = settings.getKeycloakServiceName() + "." + ISiteWhereKubernetesClient.NS_SITEWHERE_SYSTEM;
return String.format("http://%s:%s/auth", serviceName, settings.getKeycloakApiPort());
}
/*
* @see
* com.sitewhere.microservice.lifecycle.AsyncStartLifecycleComponent#start(com.
* sitewhere.spi.microservice.lifecycle.ILifecycleProgressMonitor)
*/
@Override
public void start(ILifecycleProgressMonitor monitor) throws SiteWhereException {
IInstanceSettings settings = getMicroservice().getInstanceSettings();
// Create Keycloak API client and test it.
String url = getServerUrl();
getLogger().info(String.format("Connecting to Keycloak API at '%s'.", url));
this.keycloak = KeycloakBuilder.builder().serverUrl(url).realm(settings.getKeycloakMasterRealm())
.username(settings.getKeycloakMasterUsername()).password(settings.getKeycloakMasterPassword())
.clientId("admin-cli").build();
// Wait for Keycloak connection to become available.
boolean connected = false;
while (!connected) {
ServerInfoResource server = null;
try {
server = getKeycloak().serverInfo();
if (server != null) {
SystemInfoRepresentation system = server.getInfo().getSystemInfo();
getLogger().info(String.format("Keycloak API validated as version '%s'.", system.getVersion()));
connected = true;
} else {
getLogger().info("Received null response to Keycloak server info request.");
}
} catch (ProcessingException e) {
connected = false;
}
if (!connected) {
try {
getLogger().info("Unable to connect to Keycloak. Waiting to retry...");
Thread.sleep(2000);
} catch (InterruptedException e1) {
getLogger().warn("Interrupted while waiting for Keycloak connection.");
return;
}
}
}
// Make sure that the expected realm exists.
assureRealmExists();
// Register an OpenID Connect client for the realm.
assureOpenIdClient();
}
/**
* Assure that the expected Keycloak realm exists.
*
* @throws SiteWhereException
*/
protected void assureRealmExists() throws SiteWhereException {
String realmName = getMicroservice().getInstanceSettings().getKeycloakRealm();
try {
getRealmResource().toRepresentation();
getLogger().info(String.format("Realm for instance was found (%s).", realmName));
} catch (NotFoundException e) {
getLogger().info(String.format("Realm for instance was not found (%s). Creating...", realmName));
try {
RealmRepresentation newRealm = new RealmRepresentation();
newRealm.setId(realmName);
newRealm.setRealm(realmName);
newRealm.setDisplayName("SiteWhere");
newRealm.setEnabled(true);
getKeycloak().realms().create(newRealm);
getLogger().info(String.format("Successfully created realm for instance (%s).", realmName));
} catch (ClientErrorException e1) {
Response response = e1.getResponse();
if (response.getStatus() == HttpStatus.SC_CONFLICT) {
getLogger().info(String.format("Realm for instance was found (%s).", realmName));
}
} catch (Exception e1) {
throw new SiteWhereException(String.format("Unable to create realm for instance (%s).", realmName), e1);
}
}
}
/**
* Assure that openid-connect client exists.
*
* @throws SiteWhereException
*/
protected void assureOpenIdClient() throws SiteWhereException {
try {
ClientResource clientResource = getRealmResource().clients().get(CLIENT_ID_OPENID_CONNECT);
ClientRepresentation client = clientResource.toRepresentation();
getLogger().info(String.format("OpenID Connect client was found (%s).", client.getId()));
} catch (NotFoundException e) {
getLogger().info(
String.format("OpenID Connect client was not found (%s). Creating...", CLIENT_ID_OPENID_CONNECT));
try {
ClientRepresentation newClient = new ClientRepresentation();
newClient.setId(CLIENT_ID_OPENID_CONNECT);
newClient.setName("OpenId Connect");
newClient.setStandardFlowEnabled(true);
newClient.setDirectAccessGrantsEnabled(true);
newClient.setProtocol("openid-connect");
newClient.setPublicClient(false);
newClient.setRedirectUris(Collections.singletonList("http://*"));
newClient.setSecret(getMicroservice().getInstanceSettings().getKeycloakOidcSecret());
newClient.setEnabled(true);
Response result = getRealmResource().clients().create(newClient);
if (result.getStatus() == HttpStatus.SC_CONFLICT) {
getLogger().info(String.format("Found existing OpenID Connect client (%s).", newClient.getId()));
} else if (result.getStatus() != HttpStatus.SC_CREATED) {
throw new SiteWhereException(result.getStatusInfo().getReasonPhrase());
} else {
getLogger().info(String.format("Created OpenID Connect client (%s).", newClient.getId()));
}
} catch (Exception e1) {
throw new SiteWhereException(
String.format("Unable to create realm for instance (%s).", CLIENT_ID_OPENID_CONNECT), e1);
}
} finally {
this.clientSecret = getRealmResource().clients().get(CLIENT_ID_OPENID_CONNECT).getSecret().getValue();
}
}
/**
* Get realm resource based on configured value.
*
* @return
* @throws SiteWhereException
*/
protected RealmResource getRealmResource() throws SiteWhereException {
String realmName = getMicroservice().getInstanceSettings().getKeycloakRealm();
return getKeycloak().realm(realmName);
}
/**
* Convert Keycloak user to SW model representation.
*
* @param kc
* @param includeRoles
* @param includeAuths
* @return
* @throws SiteWhereException
*/
protected User convert(UserRepresentation kc, boolean includeRoles, boolean includeAuths)
throws SiteWhereException {
User user = new User();
user.setUsername(kc.getUsername());
user.setFirstName(kc.getFirstName());
user.setLastName(kc.getLastName());
user.setEmail(kc.getEmail());
user.setCreatedDate(new Date(kc.getCreatedTimestamp()));
// Pull metadata.
if (kc.getAttributes() != null) {
user.setMetadata(new HashMap<>());
for (String key : kc.getAttributes().keySet()) {
List value = kc.getAttributes().get(key);
if (value.size() > 0) {
user.getMetadata().put(key, value.get(0));
}
}
}
// Conditionally pull roles.
if (includeRoles) {
user.setRoles(new ArrayList<>());
List groups = getRealmResource().users().get(kc.getId()).groups();
if (groups != null) {
for (GroupRepresentation group : groups) {
Role role = convert(group, includeAuths);
user.getRoles().add(role);
}
}
}
return user;
}
/**
* Convert Keycloak role to SW granted authority.
*
* @param kc
* @return
*/
protected GrantedAuthority convert(RoleRepresentation kc) {
GrantedAuthority auth = new GrantedAuthority();
auth.setAuthority(kc.getName());
auth.setDescription(kc.getDescription());
return auth;
}
/**
* Convert a Keycloak group to SW role.
*
* @param kc
* @param includeAuthorities
* @return
* @throws SiteWhereException
*/
protected Role convert(GroupRepresentation kc, boolean includeAuthorities) throws SiteWhereException {
Role role = new Role();
role.setRole(kc.getName());
role.setDescription(kc.getPath());
if (includeAuthorities) {
role.setAuthorities(new ArrayList<>());
List realmRoles = getRealmResource().groups().group(kc.getId()).roles().realmLevel()
.listEffective();
if (realmRoles != null) {
for (RoleRepresentation realmRole : realmRoles) {
role.getAuthorities().add(convert(realmRole));
}
}
}
return role;
}
/**
* Create a user representation based on API request.
*
* @param request
* @return
*/
protected UserRepresentation createUserFromRequest(IUserCreateRequest request) {
UserRepresentation user = new UserRepresentation();
user.setUsername(request.getUsername());
user.setFirstName(request.getFirstName());
user.setLastName(request.getLastName());
user.setEmail(request.getEmail());
user.setEnabled(request.isEnabled());
if (request.getPassword() != null) {
CredentialRepresentation credential = new CredentialRepresentation();
credential.setType(CredentialRepresentation.PASSWORD);
credential.setValue(request.getPassword());
user.setCredentials(Arrays.asList(credential));
}
if (request.getMetadata() != null && request.getMetadata().size() > 0) {
Map> attrs = new HashMap<>();
for (String key : request.getMetadata().keySet()) {
String value = request.getMetadata().get(key);
attrs.put(key, Collections.singletonList(value));
}
user.setAttributes(attrs);
}
return user;
}
/*
* @see
* com.sitewhere.microservice.api.user.IUserManagement#createUser(com.sitewhere.
* spi.user.request.IUserCreateRequest)
*/
@Override
public IUser createUser(IUserCreateRequest request) throws SiteWhereException {
UserRepresentation user = createUserFromRequest(request);
Response result = getRealmResource().users().create(user);
if (result.getStatus() != HttpStatus.SC_CREATED) {
throw new SiteWhereException(result.getStatusInfo().getReasonPhrase());
}
UserRepresentation match = findSingleUserByUsername(request.getUsername());
if (match != null && request.getRoles() != null) {
for (String role : request.getRoles()) {
GroupRepresentation groupMatch = findSingleGroupByName(role);
if (groupMatch != null) {
getRealmResource().users().get(match.getId()).joinGroup(groupMatch.getId());
}
}
}
return getUserByUsername(request.getUsername());
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#getAccessToken(java.lang.
* String, java.lang.String)
*/
@Override
public String getAccessToken(String username, String password) throws SiteWhereException {
IInstanceSettings settings = getMicroservice().getInstanceSettings();
Keycloak instance = Keycloak.getInstance(getServerUrl(), settings.getKeycloakRealm(), username, password,
CLIENT_ID_OPENID_CONNECT, getClientSecret());
TokenManager tokenManager = instance.tokenManager();
AccessTokenResponse accessToken = tokenManager.getAccessToken();
String json = new String(MarshalUtils.marshalJson(accessToken));
return json;
}
/*
* @see com.sitewhere.spi.microservice.user.IUserManagement#getPublicKey()
*/
@Override
public String getPublicKey() throws SiteWhereException {
List keys = getRealmResource().keys().getKeyMetadata().getKeys();
for (KeyMetadataRepresentation key : keys) {
if (key.getType().equals("RSA")) {
return key.getPublicKey();
}
}
throw new SiteWhereException("No RSA public key found.");
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#updateUser(java.lang.
* String, com.sitewhere.spi.user.request.IUserCreateRequest, boolean)
*/
@Override
public IUser updateUser(String username, IUserCreateRequest request, boolean encodePassword)
throws SiteWhereException {
UserRepresentation match = findSingleUserByUsername(username);
if (match == null) {
throw new SiteWhereException(String.format("No user found for username: %s", username));
}
UserRepresentation user = createUserFromRequest(request);
getRealmResource().users().get(match.getId()).update(user);
return getUserByUsername(request.getUsername());
}
/**
* Find a single user matching the given username.
*
* @param username
* @return
* @throws SiteWhereException
*/
protected UserRepresentation findSingleUserByUsername(String username) throws SiteWhereException {
List matches = getRealmResource().users().search(username, true);
if (matches.size() > 1) {
throw new SiteWhereException(String.format("Matched username: %s", matches.get(0).getUsername()));
} else if (matches.size() == 0) {
return null;
}
return matches.get(0);
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#getUserByUsername(java.
* lang.String)
*/
@Override
public IUser getUserByUsername(String username) throws SiteWhereException {
UserRepresentation match = findSingleUserByUsername(username);
return match != null ? convert(match, true, true) : null;
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#listUsers(com.sitewhere.
* spi.user.IUserSearchCriteria)
*/
@Override
public ISearchResults listUsers(IUserSearchCriteria criteria) throws SiteWhereException {
List kcUsers = getRealmResource().users().list();
Pager pager = new Pager(criteria);
for (UserRepresentation kcUser : kcUsers) {
IUser user = convert(kcUser, true, false);
pager.process(user);
}
return new SearchResults(pager.getResults(), pager.getTotal());
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#deleteUser(java.lang.
* String)
*/
@Override
public IUser deleteUser(String username) throws SiteWhereException {
UserRepresentation match = findSingleUserByUsername(username);
if (match == null) {
throw new SiteWhereException(String.format("No user found for username: %s", username));
}
Response result = getRealmResource().users().delete(match.getId());
if (result.getStatus() != HttpStatus.SC_NO_CONTENT) {
throw new SiteWhereException(result.getStatusInfo().getReasonPhrase());
}
return convert(match, false, false);
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#createGrantedAuthority(
* com.sitewhere.spi.user.request.IGrantedAuthorityCreateRequest)
*/
@Override
public IGrantedAuthority createGrantedAuthority(IGrantedAuthorityCreateRequest request) throws SiteWhereException {
RoleRepresentation role = new RoleRepresentation();
role.setComposite(request.isGroup());
role.setName(request.getAuthority());
role.setDescription(request.getDescription());
getRealmResource().roles().create(role);
role = getKeycloakRoleByName(request.getAuthority());
if (request.getParent() != null) {
try {
RoleRepresentation parent = getKeycloakRoleByName(request.getParent());
getRealmResource().rolesById().addComposites(parent.getId(), Collections.singletonList(role));
} catch (NotFoundException e) {
getLogger().warn(
String.format("Unable to composite role to non-existent parent: %s", request.getParent()));
}
}
return convert(role);
}
/**
* Get Keycloak role by name.
*
* @param name
* @return
* @throws SiteWhereException
*/
protected RoleRepresentation getKeycloakRoleByName(String name) throws SiteWhereException {
List all = getRealmResource().roles().list();
for (RoleRepresentation current : all) {
if (current.getName().equals(name)) {
return current;
}
}
return null;
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#getGrantedAuthorityByName
* (java.lang.String)
*/
@Override
public IGrantedAuthority getGrantedAuthorityByName(String name) throws SiteWhereException {
RoleRepresentation match = getKeycloakRoleByName(name);
return match == null ? null : convert(match);
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#updateGrantedAuthority(
* java.lang.String,
* com.sitewhere.spi.user.request.IGrantedAuthorityCreateRequest)
*/
@Override
public IGrantedAuthority updateGrantedAuthority(String name, IGrantedAuthorityCreateRequest request)
throws SiteWhereException {
throw new SiteWhereException("Not implemented.");
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#listGrantedAuthorities(
* com.sitewhere.spi.user.IGrantedAuthoritySearchCriteria)
*/
@Override
public ISearchResults listGrantedAuthorities(IGrantedAuthoritySearchCriteria criteria)
throws SiteWhereException {
List kcRoles = getRealmResource().roles().list();
Pager pager = new Pager(criteria);
for (RoleRepresentation kcRole : kcRoles) {
IGrantedAuthority authority = convert(kcRole);
pager.process(authority);
}
return new SearchResults(pager.getResults(), pager.getTotal());
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#deleteGrantedAuthority(
* java.lang.String)
*/
@Override
public void deleteGrantedAuthority(String authority) throws SiteWhereException {
getRealmResource().roles().deleteRole(authority);
}
/*
* @see com.sitewhere.spi.microservice.user.IUserManagement#getRoles(java.lang.
* String)
*/
@Override
public List getRoles(String username) throws SiteWhereException {
IUser user = getUserByUsername(username);
if (user == null) {
throw new SiteWhereException(String.format("User not found: %s", username));
}
return user.getRoles();
}
/*
* @see com.sitewhere.spi.microservice.user.IUserManagement#addRoles(java.lang.
* String, java.util.List)
*/
@Override
public List addRoles(String username, List roles) throws SiteWhereException {
UserRepresentation match = findSingleUserByUsername(username);
if (match != null) {
List created = new ArrayList<>();
for (String role : roles) {
GroupRepresentation groupMatch = findSingleGroupByName(role);
if (groupMatch != null) {
getRealmResource().users().get(match.getId()).joinGroup(groupMatch.getId());
created.add(convert(groupMatch, false));
}
}
return created;
}
throw new SiteWhereException(String.format("User not found: %s", username));
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#removeRoles(java.lang.
* String, java.util.List)
*/
@Override
public List removeRoles(String username, List roles) throws SiteWhereException {
UserRepresentation match = findSingleUserByUsername(username);
if (match != null) {
List removed = new ArrayList<>();
for (String role : roles) {
GroupRepresentation groupMatch = findSingleGroupByName(role);
if (groupMatch != null) {
getRealmResource().users().get(match.getId()).joinGroup(groupMatch.getId());
removed.add(convert(groupMatch, false));
}
}
return removed;
}
throw new SiteWhereException(String.format("User not found: %s", username));
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#createRole(com.sitewhere.
* spi.user.request.IRoleCreateRequest)
*/
@Override
public IRole createRole(IRoleCreateRequest request) throws SiteWhereException {
GroupRepresentation group = new GroupRepresentation();
group.setName(request.getRole());
Response result = getRealmResource().groups().add(group);
if (result.getStatus() != HttpStatus.SC_CREATED) {
throw new SiteWhereException(result.getStatusInfo().getReasonPhrase());
}
String location = result.getLocation().getPath();
String id = location.substring(location.lastIndexOf('/') + 1);
List roles = new ArrayList<>();
for (String authority : request.getAuthorities()) {
roles.add(getRealmResource().roles().get(authority).toRepresentation());
}
getRealmResource().groups().group(id).roles().realmLevel().add(roles);
return getRoleByName(group.getName());
}
/**
* Find single Keycloak group by name.
*
* @param name
* @return
* @throws SiteWhereException
*/
protected GroupRepresentation findSingleGroupByName(String name) throws SiteWhereException {
List matches = getRealmResource().groups().groups(name, 0, 1);
if (matches.size() > 1) {
throw new SiteWhereException(String.format("Matched multiple groups for: %s", name));
} else if (matches.size() == 0) {
return null;
}
return matches.get(0);
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#getRoleByName(java.lang.
* String)
*/
@Override
public IRole getRoleByName(String name) throws SiteWhereException {
GroupRepresentation match = findSingleGroupByName(name);
return match != null ? convert(match, true) : null;
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#updateRole(java.lang.
* String, com.sitewhere.spi.user.request.IRoleCreateRequest)
*/
@Override
public IRole updateRole(String name, IRoleCreateRequest request) throws SiteWhereException {
throw new SiteWhereException("Not implemented.");
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#listRoles(com.sitewhere.
* spi.user.IRoleSearchCriteria)
*/
@Override
public ISearchResults listRoles(IRoleSearchCriteria criteria) throws SiteWhereException {
List kcGroups = getRealmResource().groups().groups();
Pager pager = new Pager(criteria);
for (GroupRepresentation kcGroup : kcGroups) {
IRole role = convert(kcGroup, false);
pager.process(role);
}
return new SearchResults(pager.getResults(), pager.getTotal());
}
/*
* @see
* com.sitewhere.spi.microservice.user.IUserManagement#deleteRole(java.lang.
* String)
*/
@Override
public void deleteRole(String role) throws SiteWhereException {
GroupRepresentation match = findSingleGroupByName(role);
if (match != null) {
getRealmResource().groups().group(match.getId()).remove();
}
}
protected Keycloak getKeycloak() {
return keycloak;
}
protected String getClientSecret() {
return clientSecret;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy