This commit is contained in:
Bill Burke 2016-12-20 08:56:08 -05:00
commit 172007f59c
445 changed files with 17621 additions and 3065 deletions

View file

@ -57,6 +57,7 @@ public class KeycloakDeployment {
protected String resourceName;
protected boolean bearerOnly;
protected boolean autodetectBearerOnly;
protected boolean enableBasicAuth;
protected boolean publicClient;
protected Map<String, Object> resourceCredentials = new HashMap<>();
@ -201,6 +202,14 @@ public class KeycloakDeployment {
this.bearerOnly = bearerOnly;
}
public boolean isAutodetectBearerOnly() {
return autodetectBearerOnly;
}
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
this.autodetectBearerOnly = autodetectBearerOnly;
}
public boolean isEnableBasicAuth() {
return enableBasicAuth;
}

View file

@ -99,6 +99,7 @@ public class KeycloakDeploymentBuilder {
}
deployment.setBearerOnly(adapterConfig.isBearerOnly());
deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly());
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken());
deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup());

View file

@ -17,6 +17,8 @@
package org.keycloak.adapters;
import java.util.Collections;
import java.util.List;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.spi.AuthChallenge;
@ -116,6 +118,12 @@ public abstract class RequestAuthenticator {
return AuthOutcome.NOT_ATTEMPTED;
}
if (isAutodetectedBearerOnly(facade.getRequest())) {
challenge = bearer.getChallenge();
log.debug("NOT_ATTEMPTED: Treating as bearer only");
return AuthOutcome.NOT_ATTEMPTED;
}
if (log.isTraceEnabled()) {
log.trace("try oauth");
}
@ -158,6 +166,36 @@ public abstract class RequestAuthenticator {
return false;
}
protected boolean isAutodetectedBearerOnly(HttpFacade.Request request) {
if (!deployment.isAutodetectBearerOnly()) return false;
String headerValue = facade.getRequest().getHeader("X-Requested-With");
if (headerValue != null && headerValue.equalsIgnoreCase("XMLHttpRequest")) {
return true;
}
headerValue = facade.getRequest().getHeader("Faces-Request");
if (headerValue != null && headerValue.startsWith("partial/")) {
return true;
}
headerValue = facade.getRequest().getHeader("SOAPAction");
if (headerValue != null) {
return true;
}
List<String> accepts = facade.getRequest().getHeaders("Accept");
if (accepts == null) accepts = Collections.emptyList();
for (String accept : accepts) {
if (accept.contains("text/html") || accept.contains("text/*") || accept.contains("*/*")) {
return false;
}
}
return true;
}
protected abstract OAuthRequestAuthenticator createOAuthAuthenticator();
protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {

View file

@ -17,6 +17,13 @@
*/
package org.keycloak.adapters.authorization;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
@ -32,13 +39,6 @@ import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.Enforce
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@ -48,7 +48,7 @@ public abstract class AbstractPolicyEnforcer {
private final PolicyEnforcerConfig enforcerConfig;
private final PolicyEnforcer policyEnforcer;
private List<PathConfig> paths;
private Map<String, PathConfig> paths;
private AuthzClient authzClient;
private PathMatcher pathMatcher;
@ -57,7 +57,7 @@ public abstract class AbstractPolicyEnforcer {
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
this.authzClient = policyEnforcer.getClient();
this.pathMatcher = new PathMatcher();
this.paths = new ArrayList<>(policyEnforcer.getPaths());
this.paths = policyEnforcer.getPaths();
}
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
@ -75,8 +75,7 @@ public abstract class AbstractPolicyEnforcer {
if (accessToken != null) {
Request request = httpFacade.getRequest();
Response response = httpFacade.getResponse();
String pathInfo = URI.create(request.getURI()).getPath().substring(1);
String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
String path = getPath(request);
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
@ -122,12 +121,9 @@ public abstract class AbstractPolicyEnforcer {
protected boolean isAuthorized(PathConfig actualPathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
Request request = httpFacade.getRequest();
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
if (accessDeniedPath != null) {
if (request.getURI().contains(accessDeniedPath)) {
return true;
}
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
return true;
}
AccessToken.Authorization authorization = accessToken.getAuthorization();
@ -173,6 +169,17 @@ public abstract class AbstractPolicyEnforcer {
return false;
}
private boolean isDefaultAccessDeniedUri(Request request, PolicyEnforcerConfig enforcerConfig) {
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
if (accessDeniedPath != null) {
if (request.getURI().contains(accessDeniedPath)) {
return true;
}
}
return false;
}
private boolean hasResourceScopePermission(Set<String> requiredScopes, Permission permission, PathConfig actualPathConfig) {
Set<String> allowedScopes = permission.getScopes();
return (allowedScopes.containsAll(requiredScopes) || allowedScopes.isEmpty());
@ -220,27 +227,23 @@ public abstract class AbstractPolicyEnforcer {
}
private PathConfig resolvePathConfig(PathConfig originalConfig, Request request) {
String path = getPath(request);
if (originalConfig.hasPattern()) {
String pathInfo = URI.create(request.getURI()).getPath().substring(1);
String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
ProtectedResource resource = this.authzClient.protection().resource();
Set<String> search = resource.findByFilter("uri=" + path);
if (!search.isEmpty()) {
// resource does exist on the server, cache it
ResourceRepresentation targetResource = resource.findById(search.iterator().next()).getResourceDescription();
PathConfig config = new PathConfig();
PathConfig config = PolicyEnforcer.createPathConfig(targetResource);
config.setId(targetResource.getId());
config.setName(targetResource.getName());
config.setType(targetResource.getType());
config.setPath(targetResource.getUri());
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
config.setParentConfig(originalConfig);
config.setEnforcementMode(originalConfig.getEnforcementMode());
this.paths.add(config);
this.policyEnforcer.addPath(config);
return config;
}
@ -249,6 +252,11 @@ public abstract class AbstractPolicyEnforcer {
return originalConfig;
}
private String getPath(Request request) {
String pathInfo = URI.create(request.getURI()).getPath().substring(1);
return pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
}
private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) {
Set<String> requiredScopes = new HashSet<>();

View file

@ -17,6 +17,10 @@
*/
package org.keycloak.adapters.authorization;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OIDCHttpFacade;
@ -34,10 +38,6 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@ -51,39 +51,34 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
@Override
protected boolean isAuthorized(PathConfig pathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
int retry = 2;
AccessToken original = accessToken;
while (retry > 0) {
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
return true;
}
accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
if (accessToken == null) {
return false;
}
AccessToken.Authorization authorization = original.getAuthorization();
if (authorization == null) {
authorization = new AccessToken.Authorization();
authorization.setPermissions(new ArrayList<Permission>());
}
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
if (newAuthorization != null) {
authorization.getPermissions().addAll(newAuthorization.getPermissions());
}
original.setAuthorization(authorization);
retry--;
if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
return true;
}
return false;
accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
if (accessToken == null) {
return false;
}
AccessToken.Authorization authorization = original.getAuthorization();
if (authorization == null) {
authorization = new AccessToken.Authorization();
authorization.setPermissions(new ArrayList<Permission>());
}
AccessToken.Authorization newAuthorization = accessToken.getAuthorization();
if (newAuthorization != null) {
authorization.getPermissions().addAll(newAuthorization.getPermissions());
}
original.setAuthorization(authorization);
return super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade);
}
@Override
@ -108,7 +103,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
if (getEnforcerConfig().getUserManagedAccess() != null) {
LOGGER.debug("Obtaining authorization for authenticated user.");
LOGGER.debug("Obtaining authorization for authenticated user.");
PermissionRequest permissionRequest = new PermissionRequest();
permissionRequest.setResourceSetId(pathConfig.getId());
@ -136,12 +131,14 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
permissionRequest.setResourceSetId(pathConfig.getId());
permissionRequest.setResourceSetName(pathConfig.getName());
permissionRequest.setScopes(new HashSet<>(pathConfig.getScopes()));
LOGGER.debugf("Sending entitlements request: resource_set_id [%s], resource_set_name [%s], scopes [%s].", permissionRequest.getResourceSetId(), permissionRequest.getResourceSetName(), permissionRequest.getScopes());
request.addPermission(permissionRequest);
EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getClientId(), request);
return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment);
}
}
} catch (AuthorizationDeniedException e) {
LOGGER.debug("Authorization denied", e);
return null;
} catch (Exception e) {
throw new RuntimeException("Unexpected error during authorization request.", e);

View file

@ -19,7 +19,7 @@ package org.keycloak.adapters.authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -28,10 +28,16 @@ class PathMatcher {
private static final String ANY_RESOURCE_PATTERN = "/*";
PathConfig matches(final String requestedUri, List<PathConfig> paths) {
PathConfig matches(final String requestedUri, Map<String, PathConfig> paths) {
PathConfig pathConfig = paths.get(requestedUri);
if (pathConfig != null) {
return pathConfig;
}
PathConfig actualConfig = null;
for (PathConfig entry : paths) {
for (PathConfig entry : paths.values()) {
String protectedUri = entry.getPath();
String selectedUri = null;

View file

@ -34,8 +34,10 @@ import org.keycloak.representations.idm.authorization.Permission;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@ -48,7 +50,7 @@ public class PolicyEnforcer {
private final KeycloakDeployment deployment;
private final AuthzClient authzClient;
private final PolicyEnforcerConfig enforcerConfig;
private final List<PathConfig> paths;
private final Map<String, PathConfig> paths;
public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
this.deployment = deployment;
@ -58,7 +60,7 @@ public class PolicyEnforcer {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Initialization complete. Path configurations:");
for (PathConfig pathConfig : this.paths) {
for (PathConfig pathConfig : this.paths.values()) {
LOGGER.debug(pathConfig);
}
}
@ -96,15 +98,19 @@ public class PolicyEnforcer {
return authzClient;
}
public List<PathConfig> getPaths() {
return Collections.unmodifiableList(paths);
public Map<String, PathConfig> getPaths() {
return paths;
}
void addPath(PathConfig pathConfig) {
paths.put(pathConfig.getPath(), pathConfig);
}
KeycloakDeployment getDeployment() {
return deployment;
}
private List<PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
private Map<String, PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
boolean loadPathsFromServer = true;
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
@ -123,8 +129,8 @@ public class PolicyEnforcer {
}
}
private List<PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
List<PathConfig> paths = new ArrayList<>();
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
Set<String> search;
@ -172,7 +178,7 @@ public class PolicyEnforcer {
PathConfig existingPath = null;
for (PathConfig current : paths) {
for (PathConfig current : paths.values()) {
if (current.getId().equals(pathConfig.getId()) && current.getPath().equals(pathConfig.getPath())) {
existingPath = current;
break;
@ -180,7 +186,7 @@ public class PolicyEnforcer {
}
if (existingPath == null) {
paths.add(pathConfig);
paths.put(pathConfig.getPath(), pathConfig);
} else {
existingPath.getMethods().addAll(pathConfig.getMethods());
existingPath.getScopes().addAll(pathConfig.getScopes());
@ -190,23 +196,24 @@ public class PolicyEnforcer {
return paths;
}
private List<PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
private Map<String, PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
LOGGER.info("Querying the server for all resources associated with this application.");
List<PathConfig> paths = new ArrayList<>();
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
for (String id : protectedResource.findAll()) {
RegistrationResponse response = protectedResource.findById(id);
ResourceRepresentation resourceDescription = response.getResourceDescription();
if (resourceDescription.getUri() != null) {
paths.add(createPathConfig(resourceDescription));
PathConfig pathConfig = createPathConfig(resourceDescription);
paths.put(pathConfig.getPath(), pathConfig);
}
}
return paths;
}
private PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
PathConfig pathConfig = new PathConfig();
pathConfig.setId(resourceDescription.getId());

View file

@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Keycloak Integration</name>
<name>Keycloak Adapters</name>
<description/>
<modelVersion>4.0.0</modelVersion>

View file

@ -40,7 +40,7 @@ public class SamlUtil {
httpFacade.getResponse().setHeader("Content-Type", "text/html");
httpFacade.getResponse().setHeader("Pragma", "no-cache");
httpFacade.getResponse().setHeader("Cache-Control", "no-cache, no-store");
httpFacade.getResponse().getOutputStream().write(html.getBytes());
httpFacade.getResponse().getOutputStream().write(html.getBytes(GeneralConstants.SAML_CHARSET));
httpFacade.getResponse().end();
} else {
String uri = asRequest ? binding.redirectBinding(document).requestURI(actionUrl).toString() : binding.redirectBinding(document).responseURI(actionUrl).toString();

View file

@ -571,12 +571,8 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic
key = locator.getKey(keyId);
boolean keyLocated = key != null;
if (validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key)) {
return true;
}
if (keyLocated) {
return false;
return validateRedirectBindingSignatureForKey(sigAlg, rawQueryBytes, decodedSignature, key);
}
} catch (KeyManagementException ex) {
}

View file

@ -17,6 +17,8 @@
*/
package org.keycloak.authorization.policy.provider.aggregated;
import java.util.List;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
@ -26,21 +28,11 @@ import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import java.util.List;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class AggregatePolicyProvider implements PolicyProvider {
private final Policy policy;
private final AuthorizationProvider authorization;
public AggregatePolicyProvider(Policy policy, AuthorizationProvider authorization) {
this.policy = policy;
this.authorization = authorization;
}
@Override
public void evaluate(Evaluation evaluation) {
//TODO: need to detect deep recursions
@ -59,10 +51,12 @@ public class AggregatePolicyProvider implements PolicyProvider {
}
};
this.policy.getAssociatedPolicies().forEach(associatedPolicy -> {
PolicyProviderFactory providerFactory = authorization.getProviderFactory(associatedPolicy.getType());
PolicyProvider policyProvider = providerFactory.create(associatedPolicy, authorization);
policyProvider.evaluate(new DefaultEvaluation(evaluation.getPermission(), evaluation.getContext(), policy, associatedPolicy, decision));
Policy policy = evaluation.getPolicy();
AuthorizationProvider authorization = evaluation.getAuthorizationProvider();
policy.getAssociatedPolicies().forEach(associatedPolicy -> {
PolicyProvider policyProvider = authorization.getProvider(associatedPolicy.getType());
policyProvider.evaluate(new DefaultEvaluation(evaluation.getPermission(), evaluation.getContext(), policy, associatedPolicy, decision, authorization));
});
decision.onComplete();

View file

@ -19,7 +19,6 @@ package org.keycloak.authorization.policy.provider.aggregated;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@ -32,6 +31,8 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class AggregatePolicyProviderFactory implements PolicyProviderFactory {
private AggregatePolicyProvider provider = new AggregatePolicyProvider();
@Override
public String getName() {
return "Aggregated";
@ -43,8 +44,8 @@ public class AggregatePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
return new AggregatePolicyProvider(policy, authorization);
public PolicyProvider create(AuthorizationProvider authorization) {
return provider;
}
@Override

View file

@ -1,5 +1,7 @@
package org.keycloak.authorization.policy.provider.client;
import static org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory.getClients;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
@ -8,26 +10,19 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import static org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory.getClients;
public class ClientPolicyProvider implements PolicyProvider {
private final Policy policy;
private final AuthorizationProvider authorization;
public ClientPolicyProvider(Policy policy, AuthorizationProvider authorization) {
this.policy = policy;
this.authorization = authorization;
}
@Override
public void evaluate(Evaluation evaluation) {
Policy policy = evaluation.getPolicy();
EvaluationContext context = evaluation.getContext();
String[] clients = getClients(this.policy);
String[] clients = getClients(policy);
AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider();
RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm();
if (clients.length > 0) {
for (String client : clients) {
ClientModel clientModel = getCurrentRealm().getClientById(client);
ClientModel clientModel = realm.getClientById(client);
if (context.getAttributes().containsValue("kc.client.id", clientModel.getClientId())) {
evaluation.grant();
return;
@ -40,8 +35,4 @@ public class ClientPolicyProvider implements PolicyProvider {
public void close() {
}
private RealmModel getCurrentRealm() {
return this.authorization.getKeycloakSession().getContext().getRealm();
}
}

View file

@ -1,5 +1,9 @@
package org.keycloak.authorization.policy.provider.client;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@ -8,18 +12,18 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel.ClientRemovedEvent;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ClientPolicyProviderFactory implements PolicyProviderFactory {
private ClientPolicyProvider provider = new ClientPolicyProvider();
@Override
public String getName() {
return "Client";
@ -31,8 +35,8 @@ public class ClientPolicyProviderFactory implements PolicyProviderFactory {
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
return new ClientPolicyProvider(policy, authorization);
public PolicyProvider create(AuthorizationProvider authorization) {
return provider;
}
@Override
@ -56,31 +60,36 @@ public class ClientPolicyProviderFactory implements PolicyProviderFactory {
if (event instanceof ClientRemovedEvent) {
KeycloakSession keycloakSession = ((ClientRemovedEvent) event).getKeycloakSession();
AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
StoreFactory storeFactory = provider.getStoreFactory();
PolicyStore policyStore = storeFactory.getPolicyStore();
ClientModel removedClient = ((ClientRemovedEvent) event).getClient();
ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
ResourceServer resourceServer = resourceServerStore.findByClient(removedClient.getId());
policyStore.findByType(getId()).forEach(policy -> {
List<String> clients = new ArrayList<>();
if (resourceServer != null) {
policyStore.findByType(getId(), resourceServer.getId()).forEach(policy -> {
List<String> clients = new ArrayList<>();
for (String clientId : getClients(policy)) {
if (!clientId.equals(removedClient.getId())) {
clients.add(clientId);
for (String clientId : getClients(policy)) {
if (!clientId.equals(removedClient.getId())) {
clients.add(clientId);
}
}
}
try {
if (clients.isEmpty()) {
policyStore.findDependentPolicies(policy.getId()).forEach(dependentPolicy -> {
dependentPolicy.removeAssociatedPolicy(policy);
});
policyStore.delete(policy.getId());
} else {
policy.getConfig().put("clients", JsonSerialization.writeValueAsString(clients));
try {
if (clients.isEmpty()) {
policyStore.findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> {
dependentPolicy.removeAssociatedPolicy(policy);
});
policyStore.delete(policy.getId());
} else {
policy.getConfig().put("clients", JsonSerialization.writeValueAsString(clients));
}
} catch (IOException e) {
throw new RuntimeException("Error while synchronizing clients with policy [" + policy.getName() + "].", e);
}
} catch (IOException e) {
throw new RuntimeException("Error while synchronizing clients with policy [" + policy.getName() + "].", e);
}
});
});
}
}
});
}

View file

@ -17,32 +17,34 @@
*/
package org.keycloak.authorization.policy.provider.js;
import java.util.function.Supplier;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class JSPolicyProvider implements PolicyProvider {
private final Policy policy;
private Supplier<ScriptEngine> engineProvider;
public JSPolicyProvider(Policy policy) {
this.policy = policy;
public JSPolicyProvider(Supplier<ScriptEngine> engineProvider) {
this.engineProvider = engineProvider;
}
@Override
public void evaluate(Evaluation evaluation) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
ScriptEngine engine = engineProvider.get();
engine.put("$evaluation", evaluation);
Policy policy = evaluation.getPolicy();
try {
engine.eval(policy.getConfig().get("code"));
} catch (ScriptException e) {

View file

@ -1,8 +1,12 @@
package org.keycloak.authorization.policy.provider.js;
import java.util.function.Supplier;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@ -15,6 +19,13 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class JSPolicyProviderFactory implements PolicyProviderFactory {
private JSPolicyProvider provider = new JSPolicyProvider(new Supplier<ScriptEngine>() {
@Override
public ScriptEngine get() {
return new ScriptEngineManager().getEngineByName("nashorn");
}
});
@Override
public String getName() {
return "JavaScript";
@ -26,8 +37,8 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory {
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
return new JSPolicyProvider(policy);
public PolicyProvider create(AuthorizationProvider authorization) {
return provider;
}
@Override

View file

@ -17,7 +17,6 @@
*/
package org.keycloak.authorization.policy.provider.resource;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
@ -26,7 +25,7 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
*/
public class ResourcePolicyProvider implements PolicyProvider {
public ResourcePolicyProvider(Policy policy) {
public ResourcePolicyProvider() {
}

View file

@ -2,7 +2,6 @@ package org.keycloak.authorization.policy.provider.resource;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@ -15,6 +14,8 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class ResourcePolicyProviderFactory implements PolicyProviderFactory {
private ResourcePolicyProvider provider = new ResourcePolicyProvider();
@Override
public String getName() {
return "Resource-Based";
@ -26,8 +27,8 @@ public class ResourcePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
return new ResourcePolicyProvider(policy);
public PolicyProvider create(AuthorizationProvider authorization) {
return provider;
}
@Override

View file

@ -17,6 +17,10 @@
*/
package org.keycloak.authorization.policy.provider.role;
import static org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory.getRoles;
import java.util.Map;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Policy;
@ -26,39 +30,26 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import java.util.Map;
import static org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory.getRoles;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class RolePolicyProvider implements PolicyProvider {
private final Policy policy;
private final AuthorizationProvider authorization;
public RolePolicyProvider(Policy policy, AuthorizationProvider authorization) {
this.policy = policy;
this.authorization = authorization;
}
public RolePolicyProvider() {
this(null, null);
}
@Override
public void evaluate(Evaluation evaluation) {
Map<String, Object>[] roleIds = getRoles(this.policy);
Policy policy = evaluation.getPolicy();
Map<String, Object>[] roleIds = getRoles(policy);
AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider();
RealmModel realm = authorizationProvider.getKeycloakSession().getContext().getRealm();
if (roleIds.length > 0) {
Identity identity = evaluation.getContext().getIdentity();
for (Map<String, Object> current : roleIds) {
RoleModel role = getCurrentRealm().getRoleById((String) current.get("id"));
RoleModel role = realm.getRoleById((String) current.get("id"));
if (role != null) {
boolean hasRole = hasRole(identity, role);
boolean hasRole = hasRole(identity, role, realm);
if (!hasRole && Boolean.valueOf(isRequired(current))) {
evaluation.deny();
@ -75,19 +66,15 @@ public class RolePolicyProvider implements PolicyProvider {
return (boolean) current.getOrDefault("required", false);
}
private boolean hasRole(Identity identity, RoleModel role) {
private boolean hasRole(Identity identity, RoleModel role, RealmModel realm) {
String roleName = role.getName();
if (role.isClientRole()) {
ClientModel clientModel = getCurrentRealm().getClientById(role.getContainerId());
ClientModel clientModel = realm.getClientById(role.getContainerId());
return identity.hasClientRole(clientModel.getClientId(), roleName);
}
return identity.hasRealmRole(roleName);
}
private RealmModel getCurrentRealm() {
return this.authorization.getKeycloakSession().getContext().getRealm();
}
@Override
public void close() {

View file

@ -26,8 +26,13 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleContainerModel.RoleRemovedEvent;
import org.keycloak.models.RoleModel;
import org.keycloak.util.JsonSerialization;
@ -37,12 +42,15 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class RolePolicyProviderFactory implements PolicyProviderFactory {
private RolePolicyProvider provider = new RolePolicyProvider();
@Override
public String getName() {
return "Role";
@ -54,8 +62,8 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
return new RolePolicyProvider(policy, authorization);
public PolicyProvider create(AuthorizationProvider authorization) {
return provider;
}
@Override
@ -79,43 +87,63 @@ public class RolePolicyProviderFactory implements PolicyProviderFactory {
if (event instanceof RoleRemovedEvent) {
KeycloakSession keycloakSession = ((RoleRemovedEvent) event).getKeycloakSession();
AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
StoreFactory storeFactory = provider.getStoreFactory();
PolicyStore policyStore = storeFactory.getPolicyStore();
RoleModel removedRole = ((RoleRemovedEvent) event).getRole();
RoleContainerModel container = removedRole.getContainer();
ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
policyStore.findByType(getId()).forEach(policy -> {
List<Map> roles = new ArrayList<>();
for (Map<String,Object> role : getRoles(policy)) {
if (!role.get("id").equals(removedRole.getId())) {
Map updated = new HashMap();
updated.put("id", role.get("id"));
Object required = role.get("required");
if (required != null) {
updated.put("required", required);
}
roles.add(updated);
}
}
try {
if (roles.isEmpty()) {
policyStore.findDependentPolicies(policy.getId()).forEach(dependentPolicy -> {
dependentPolicy.removeAssociatedPolicy(policy);
});
policyStore.delete(policy.getId());
} else {
Map<String, String> config = policy.getConfig();
config.put("roles", JsonSerialization.writeValueAsString(roles));
policy.setConfig(config);
}
} catch (IOException e) {
throw new RuntimeException("Error while synchronizing roles with policy [" + policy.getName() + "].", e);
}
});
if (container instanceof RealmModel) {
RealmModel realm = (RealmModel) container;
realm.getClients().forEach(clientModel -> updateResourceServer(clientModel, removedRole, resourceServerStore, policyStore));
} else {
ClientModel clientModel = (ClientModel) container;
updateResourceServer(clientModel, removedRole, resourceServerStore, policyStore);
}
}
});
}
private void updateResourceServer(ClientModel clientModel, RoleModel removedRole, ResourceServerStore resourceServerStore, PolicyStore policyStore) {
ResourceServer resourceServer = resourceServerStore.findByClient(clientModel.getId());
if (resourceServer != null) {
policyStore.findByType(getId(), resourceServer.getId()).forEach(policy -> {
List<Map> roles = new ArrayList<>();
for (Map<String,Object> role : getRoles(policy)) {
if (!role.get("id").equals(removedRole.getId())) {
Map updated = new HashMap();
updated.put("id", role.get("id"));
Object required = role.get("required");
if (required != null) {
updated.put("required", required);
}
roles.add(updated);
}
}
try {
if (roles.isEmpty()) {
policyStore.findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> {
dependentPolicy.removeAssociatedPolicy(policy);
if (dependentPolicy.getAssociatedPolicies().isEmpty()) {
policyStore.delete(dependentPolicy.getId());
}
});
policyStore.delete(policy.getId());
} else {
Map<String, String> config = policy.getConfig();
config.put("roles", JsonSerialization.writeValueAsString(roles));
policy.setConfig(config);
}
} catch (IOException e) {
throw new RuntimeException("Error while synchronizing roles with policy [" + policy.getName() + "].", e);
}
});
}
}
@Override
public void close() {

View file

@ -17,7 +17,6 @@
*/
package org.keycloak.authorization.policy.provider.scope;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
@ -26,12 +25,6 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
*/
public class ScopePolicyProvider implements PolicyProvider {
private final Policy policy;
public ScopePolicyProvider(Policy policy) {
this.policy = policy;
}
@Override
public void evaluate(Evaluation evaluation) {
}

View file

@ -2,7 +2,6 @@ package org.keycloak.authorization.policy.provider.scope;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@ -15,6 +14,8 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class ScopePolicyProviderFactory implements PolicyProviderFactory {
private ScopePolicyProvider provider = new ScopePolicyProvider();
@Override
public String getName() {
return "Scope-Based";
@ -26,8 +27,8 @@ public class ScopePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
return new ScopePolicyProvider(policy);
public PolicyProvider create(AuthorizationProvider authorization) {
return provider;
}
@Override

View file

@ -17,14 +17,14 @@
*/
package org.keycloak.authorization.policy.provider.time;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@ -32,51 +32,51 @@ public class TimePolicyProvider implements PolicyProvider {
static String DEFAULT_DATE_PATTERN = "yyyy-MM-dd hh:mm:ss";
private final Policy policy;
private final SimpleDateFormat dateFormat;
private final Date currentDate;
public TimePolicyProvider(Policy policy) {
this.policy = policy;
public TimePolicyProvider() {
this.dateFormat = new SimpleDateFormat(DEFAULT_DATE_PATTERN);
this.currentDate = new Date();
}
@Override
public void evaluate(Evaluation evaluation) {
Policy policy = evaluation.getPolicy();
try {
String notBefore = this.policy.getConfig().get("nbf");
if (notBefore != null) {
String notBefore = policy.getConfig().get("nbf");
if (notBefore != null && !"".equals(notBefore)) {
if (this.currentDate.before(this.dateFormat.parse(format(notBefore)))) {
evaluation.deny();
return;
}
}
String notOnOrAfter = this.policy.getConfig().get("noa");
if (notOnOrAfter != null) {
String notOnOrAfter = policy.getConfig().get("noa");
if (notOnOrAfter != null && !"".equals(notOnOrAfter)) {
if (this.currentDate.after(this.dateFormat.parse(format(notOnOrAfter)))) {
evaluation.deny();
return;
}
}
if (isInvalid(Calendar.DAY_OF_MONTH, "dayMonth")
|| isInvalid(Calendar.MONTH, "month")
|| isInvalid(Calendar.YEAR, "year")
|| isInvalid(Calendar.HOUR_OF_DAY, "hour")
|| isInvalid(Calendar.MINUTE, "minute")) {
if (isInvalid(Calendar.DAY_OF_MONTH, "dayMonth", policy)
|| isInvalid(Calendar.MONTH, "month", policy)
|| isInvalid(Calendar.YEAR, "year", policy)
|| isInvalid(Calendar.HOUR_OF_DAY, "hour", policy)
|| isInvalid(Calendar.MINUTE, "minute", policy)) {
evaluation.deny();
return;
}
evaluation.grant();
} catch (Exception e) {
throw new RuntimeException("Could not evaluate time-based policy [" + this.policy.getName() + "].", e);
throw new RuntimeException("Could not evaluate time-based policy [" + policy.getName() + "].", e);
}
}
private boolean isInvalid(int timeConstant, String configName) {
private boolean isInvalid(int timeConstant, String configName, Policy policy) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(this.currentDate);
@ -87,9 +87,9 @@ public class TimePolicyProvider implements PolicyProvider {
dateField++;
}
String start = this.policy.getConfig().get(configName);
String start = policy.getConfig().get(configName);
if (start != null) {
String end = this.policy.getConfig().get(configName + "End");
String end = policy.getConfig().get(configName + "End");
if (end != null) {
if (dateField < Integer.parseInt(start) || dateField > Integer.parseInt(end)) {
return true;

View file

@ -2,7 +2,6 @@ package org.keycloak.authorization.policy.provider.time;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
@ -15,6 +14,8 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class TimePolicyProviderFactory implements PolicyProviderFactory {
private TimePolicyProvider provider = new TimePolicyProvider();
@Override
public String getName() {
return "Time";
@ -26,8 +27,8 @@ public class TimePolicyProviderFactory implements PolicyProviderFactory {
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
return new TimePolicyProvider(policy);
public PolicyProvider create(AuthorizationProvider authorization) {
return provider;
}
@Override

View file

@ -29,16 +29,11 @@ import static org.keycloak.authorization.policy.provider.user.UserPolicyProvider
*/
public class UserPolicyProvider implements PolicyProvider {
private final Policy policy;
public UserPolicyProvider(Policy policy) {
this.policy = policy;
}
@Override
public void evaluate(Evaluation evaluation) {
Policy policy = evaluation.getPolicy();
EvaluationContext context = evaluation.getContext();
String[] userIds = getUsers(this.policy);
String[] userIds = getUsers(policy);
if (userIds.length > 0) {
for (String userId : userIds) {

View file

@ -18,6 +18,10 @@
package org.keycloak.authorization.policy.provider.user;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@ -26,21 +30,22 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.UserRemovedEvent;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class UserPolicyProviderFactory implements PolicyProviderFactory {
private UserPolicyProvider provider = new UserPolicyProvider();
@Override
public String getName() {
return "User";
@ -52,8 +57,8 @@ public class UserPolicyProviderFactory implements PolicyProviderFactory {
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
return new UserPolicyProvider(policy);
public PolicyProvider create(AuthorizationProvider authorization) {
return provider;
}
@Override
@ -77,29 +82,40 @@ public class UserPolicyProviderFactory implements PolicyProviderFactory {
if (event instanceof UserRemovedEvent) {
KeycloakSession keycloakSession = ((UserRemovedEvent) event).getKeycloakSession();
AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
StoreFactory storeFactory = provider.getStoreFactory();
PolicyStore policyStore = storeFactory.getPolicyStore();
UserModel removedUser = ((UserRemovedEvent) event).getUser();
RealmModel realm = ((UserRemovedEvent) event).getRealm();
ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
realm.getClients().forEach(clientModel -> {
ResourceServer resourceServer = resourceServerStore.findByClient(clientModel.getId());
policyStore.findByType(getId()).forEach(policy -> {
List<String> users = new ArrayList<>();
if (resourceServer != null) {
policyStore.findByType(getId(), resourceServer.getId()).forEach(policy -> {
List<String> users = new ArrayList<>();
for (String userId : getUsers(policy)) {
if (!userId.equals(removedUser.getId())) {
users.add(userId);
}
}
for (String userId : getUsers(policy)) {
if (!userId.equals(removedUser.getId())) {
users.add(userId);
}
}
try {
if (users.isEmpty()) {
policyStore.findDependentPolicies(policy.getId()).forEach(dependentPolicy -> {
dependentPolicy.removeAssociatedPolicy(policy);
});
policyStore.delete(policy.getId());
} else {
policy.getConfig().put("users", JsonSerialization.writeValueAsString(users));
}
} catch (IOException e) {
throw new RuntimeException("Error while synchronizing users with policy [" + policy.getName() + "].", e);
try {
if (users.isEmpty()) {
policyStore.findDependentPolicies(policy.getId(), resourceServer.getId()).forEach(dependentPolicy -> {
dependentPolicy.removeAssociatedPolicy(policy);
if (dependentPolicy.getAssociatedPolicies().isEmpty()) {
policyStore.delete(dependentPolicy.getId());
}
});
policyStore.delete(policy.getId());
} else {
policy.getConfig().put("users", JsonSerialization.writeValueAsString(users));
}
} catch (IOException e) {
throw new RuntimeException("Error while synchronizing users with policy [" + policy.getName() + "].", e);
}
});
}
});
}

View file

@ -17,6 +17,9 @@
*/
package org.keycloak.authorization.policy.provider.drools;
import java.util.function.Function;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.policy.evaluation.Evaluation;
import org.keycloak.authorization.policy.provider.PolicyProvider;
@ -25,15 +28,15 @@ import org.keycloak.authorization.policy.provider.PolicyProvider;
*/
public class DroolsPolicyProvider implements PolicyProvider {
private final DroolsPolicy policy;
private final Function<Policy, DroolsPolicy> policy;
public DroolsPolicyProvider(DroolsPolicy policy) {
this.policy = policy;
public DroolsPolicyProvider(Function<Policy, DroolsPolicy> policyProvider) {
this.policy = policyProvider;
}
@Override
public void evaluate(Evaluation evaluationt) {
this.policy.evaluate(evaluationt);
public void evaluate(Evaluation evaluation) {
policy.apply(evaluation.getPolicy()).evaluate(evaluation);
}
@Override

View file

@ -1,5 +1,9 @@
package org.keycloak.authorization.policy.provider.drools;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.keycloak.Config;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
@ -9,24 +13,25 @@ import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.provider.ProviderEventListener;
import org.keycloak.provider.ProviderFactory;
import org.kie.api.KieServices;
import org.kie.api.KieServices.Factory;
import org.kie.api.runtime.KieContainer;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
private KieServices ks;
private final Map<String, DroolsPolicy> containers = new HashMap<>();
private final Map<String, DroolsPolicy> containers = Collections.synchronizedMap(new HashMap<>());
private DroolsPolicyProvider provider = new DroolsPolicyProvider(policy -> {
if (!containers.containsKey(policy.getId())) {
synchronized (containers) {
update(policy);
}
}
return containers.get(policy.getId());
});
@Override
public String getName() {
@ -39,12 +44,8 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
}
@Override
public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
if (!this.containers.containsKey(policy.getId())) {
update(policy);
}
return new DroolsPolicyProvider(this.containers.get(policy.getId()));
public PolicyProvider create(AuthorizationProvider authorization) {
return provider;
}
@Override
@ -64,19 +65,6 @@ public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
@Override
public void postInit(KeycloakSessionFactory factory) {
factory.register(new ProviderEventListener() {
@Override
public void onEvent(ProviderEvent event) {
// Ensure the initialization is done after DB upgrade is finished
if (event instanceof PostMigrationEvent) {
ProviderFactory<AuthorizationProvider> providerFactory = factory.getProviderFactory(AuthorizationProvider.class);
AuthorizationProvider authorization = providerFactory.create(factory.create());
authorization.getStoreFactory().getPolicyStore().findByType(getId()).forEach(DroolsPolicyProviderFactory.this::update);
}
}
});
}
@Override

View file

@ -19,7 +19,14 @@ package org.keycloak.common;
import java.io.File;
import java.io.FileInputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -27,43 +34,87 @@ import java.util.Properties;
*/
public class Profile {
private enum ProfileValue {
PRODUCT, PREVIEW, COMMUNITY
public enum Feature {
AUTHORIZATION, IMPERSONATION, SCRIPTS
}
private static ProfileValue value = load();
private enum ProfileValue {
PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS),
PREVIEW,
COMMUNITY;
static ProfileValue load() {
String profile = null;
private List<Feature> disabled;
ProfileValue() {
this.disabled = Collections.emptyList();
}
ProfileValue(Feature... disabled) {
this.disabled = Arrays.asList(disabled);
}
}
private static final Profile CURRENT = new Profile();
private final ProfileValue profile;
private final Set<Feature> disabledFeatures = new HashSet<>();
private Profile() {
try {
profile = System.getProperty("keycloak.profile");
if (profile == null) {
String jbossServerConfigDir = System.getProperty("jboss.server.config.dir");
if (jbossServerConfigDir != null) {
File file = new File(jbossServerConfigDir, "profile.properties");
if (file.isFile()) {
Properties props = new Properties();
props.load(new FileInputStream(file));
profile = props.getProperty("profile");
Properties props = new Properties();
String jbossServerConfigDir = System.getProperty("jboss.server.config.dir");
if (jbossServerConfigDir != null) {
File file = new File(jbossServerConfigDir, "profile.properties");
if (file.isFile()) {
props.load(new FileInputStream(file));
}
}
if (System.getProperties().containsKey("keycloak.profile")) {
props.setProperty("profile", System.getProperty("keycloak.profile"));
}
for (String k : System.getProperties().stringPropertyNames()) {
if (k.startsWith("keycloak.profile.feature.")) {
props.put(k.replace("keycloak.profile.feature.", "feature."), System.getProperty(k));
}
}
if (props.containsKey("profile")) {
profile = ProfileValue.valueOf(props.getProperty("profile").toUpperCase());
} else {
profile = ProfileValue.valueOf(Version.DEFAULT_PROFILE.toUpperCase());
}
disabledFeatures.addAll(profile.disabled);
for (String k : props.stringPropertyNames()) {
if (k.startsWith("feature.")) {
Feature f = Feature.valueOf(k.replace("feature.", "").toUpperCase());
if (props.get(k).equals("enabled")) {
disabledFeatures.remove(f);
} else if (props.get(k).equals("disabled")) {
disabledFeatures.add(f);
}
}
}
} catch (Exception e) {
}
if (profile == null) {
return ProfileValue.valueOf(Version.DEFAULT_PROFILE.toUpperCase());
} else {
return ProfileValue.valueOf(profile.toUpperCase());
throw new RuntimeException(e);
}
}
public static String getName() {
return value.name().toLowerCase();
return CURRENT.profile.name().toLowerCase();
}
public static boolean isPreviewEnabled() {
return value.ordinal() >= ProfileValue.PREVIEW.ordinal();
public static Set<Feature> getDisabledFeatures() {
return CURRENT.disabledFeatures;
}
public static boolean isFeatureEnabled(Feature feature) {
return !CURRENT.disabledFeatures.contains(feature);
}
}

View file

@ -17,6 +17,8 @@
package org.keycloak.common.util;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
@ -38,6 +40,10 @@ public class KeyUtils {
private KeyUtils() {
}
public static SecretKey loadSecretKey(String secret) {
return new SecretKeySpec(secret.getBytes(), "HmacSHA256");
}
public static KeyPair generateRsaKeyPair(int keysize) {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");

View file

@ -21,6 +21,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -28,18 +29,41 @@ import java.io.InputStreamReader;
*/
public final class StreamUtil {
private static final int BUFFER_LENGTH = 4096;
private StreamUtil() {
}
/**
* Reads string from byte input stream.
* @param in InputStream to build the String from
* @return String representation of the input stream contents decoded using default charset
* @throws IOException
* @deprecated Use {@link #readString(java.io.InputStream, java.nio.charset.Charset)} variant.
*/
@Deprecated
public static String readString(InputStream in) throws IOException
{
char[] buffer = new char[1024];
return readString(in, Charset.defaultCharset());
}
/**
* Reads string from byte input stream.
* @param in InputStream to build the String from
* @param charset Charset used to decode the input stream
* @return String representation of the input stream contents decoded using given charset
* @throws IOException
* @deprecated Use {@link #readString(java.io.InputStream, java.nio.charset.Charset)} variant.
*/
public static String readString(InputStream in, Charset charset) throws IOException
{
char[] buffer = new char[BUFFER_LENGTH];
StringBuilder builder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
BufferedReader reader = new BufferedReader(new InputStreamReader(in, charset));
int wasRead;
do
{
wasRead = reader.read(buffer, 0, 1024);
wasRead = reader.read(buffer, 0, BUFFER_LENGTH);
if (wasRead > 0)
{
builder.append(buffer, 0, wasRead);

View file

@ -24,6 +24,7 @@ import org.keycloak.representations.idm.authorization.Permission;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -31,10 +32,10 @@ import java.util.List;
public class AuthorizationContext {
private final AccessToken authzToken;
private final List<PathConfig> paths;
private final Map<String, PathConfig> paths;
private boolean granted;
public AuthorizationContext(AccessToken authzToken, List<PathConfig> paths) {
public AuthorizationContext(AccessToken authzToken, Map<String, PathConfig> paths) {
this.authzToken = authzToken;
this.paths = paths;
this.granted = true;
@ -57,7 +58,7 @@ public class AuthorizationContext {
}
for (Permission permission : authorization.getPermissions()) {
for (PathConfig pathHolder : this.paths) {
for (PathConfig pathHolder : this.paths.values()) {
if (pathHolder.getName().equals(resourceName)) {
if (pathHolder.getId().equals(permission.getResourceSetId())) {
if (permission.getScopes().contains(scopeName)) {
@ -83,7 +84,7 @@ public class AuthorizationContext {
}
for (Permission permission : authorization.getPermissions()) {
for (PathConfig pathHolder : this.paths) {
for (PathConfig pathHolder : this.paths.values()) {
if (pathHolder.getName().equals(resourceName)) {
if (pathHolder.getId().equals(permission.getResourceSetId())) {
return true;

View file

@ -19,11 +19,7 @@ package org.keycloak;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.TokenUtil;
import java.security.PublicKey;
@ -33,18 +29,10 @@ import java.security.PublicKey;
*/
public class RSATokenVerifier {
private final String tokenString;
private PublicKey publicKey;
private String realmUrl;
private boolean checkTokenType = true;
private boolean checkActive = true;
private boolean checkRealmUrl = true;
private JWSInput jws;
private AccessToken token;
private TokenVerifier tokenVerifier;
private RSATokenVerifier(String tokenString) {
this.tokenString = tokenString;
this.tokenVerifier = TokenVerifier.create(tokenString);
}
public static RSATokenVerifier create(String tokenString) {
@ -60,94 +48,45 @@ public class RSATokenVerifier {
}
public RSATokenVerifier publicKey(PublicKey publicKey) {
this.publicKey = publicKey;
tokenVerifier.publicKey(publicKey);
return this;
}
public RSATokenVerifier realmUrl(String realmUrl) {
this.realmUrl = realmUrl;
tokenVerifier.realmUrl(realmUrl);
return this;
}
public RSATokenVerifier checkTokenType(boolean checkTokenType) {
this.checkTokenType = checkTokenType;
tokenVerifier.checkTokenType(checkTokenType);
return this;
}
public RSATokenVerifier checkActive(boolean checkActive) {
this.checkActive = checkActive;
tokenVerifier.checkActive(checkActive);
return this;
}
public RSATokenVerifier checkRealmUrl(boolean checkRealmUrl) {
this.checkRealmUrl = checkRealmUrl;
tokenVerifier.checkRealmUrl(checkRealmUrl);
return this;
}
public RSATokenVerifier parse() throws VerificationException {
if (jws == null) {
if (tokenString == null) {
throw new VerificationException("Token not set");
}
try {
jws = new JWSInput(tokenString);
} catch (JWSInputException e) {
throw new VerificationException("Failed to parse JWT", e);
}
try {
token = jws.readJsonContent(AccessToken.class);
} catch (JWSInputException e) {
throw new VerificationException("Failed to read access token from JWT", e);
}
}
tokenVerifier.parse();
return this;
}
public AccessToken getToken() throws VerificationException {
parse();
return token;
return tokenVerifier.getToken();
}
public JWSHeader getHeader() throws VerificationException {
parse();
return jws.getHeader();
return tokenVerifier.getHeader();
}
public RSATokenVerifier verify() throws VerificationException {
parse();
if (publicKey == null) {
throw new VerificationException("Public key not set");
}
if (checkRealmUrl && realmUrl == null) {
throw new VerificationException("Realm URL not set");
}
if (!RSAProvider.verify(jws, publicKey)) {
throw new VerificationException("Invalid token signature");
}
String user = token.getSubject();
if (user == null) {
throw new VerificationException("Subject missing in token");
}
if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
}
if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
}
if (checkActive && !token.isActive()) {
throw new VerificationException("Token is not active");
}
tokenVerifier.verify();
return this;
}

View file

@ -0,0 +1,170 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.TokenUtil;
import javax.crypto.SecretKey;
import java.security.PublicKey;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TokenVerifier {
private final String tokenString;
private PublicKey publicKey;
private SecretKey secretKey;
private String realmUrl;
private boolean checkTokenType = true;
private boolean checkActive = true;
private boolean checkRealmUrl = true;
private JWSInput jws;
private AccessToken token;
protected TokenVerifier(String tokenString) {
this.tokenString = tokenString;
}
public static TokenVerifier create(String tokenString) {
return new TokenVerifier(tokenString);
}
public TokenVerifier publicKey(PublicKey publicKey) {
this.publicKey = publicKey;
return this;
}
public TokenVerifier secretKey(SecretKey secretKey) {
this.secretKey = secretKey;
return this;
}
public TokenVerifier realmUrl(String realmUrl) {
this.realmUrl = realmUrl;
return this;
}
public TokenVerifier checkTokenType(boolean checkTokenType) {
this.checkTokenType = checkTokenType;
return this;
}
public TokenVerifier checkActive(boolean checkActive) {
this.checkActive = checkActive;
return this;
}
public TokenVerifier checkRealmUrl(boolean checkRealmUrl) {
this.checkRealmUrl = checkRealmUrl;
return this;
}
public TokenVerifier parse() throws VerificationException {
if (jws == null) {
if (tokenString == null) {
throw new VerificationException("Token not set");
}
try {
jws = new JWSInput(tokenString);
} catch (JWSInputException e) {
throw new VerificationException("Failed to parse JWT", e);
}
try {
token = jws.readJsonContent(AccessToken.class);
} catch (JWSInputException e) {
throw new VerificationException("Failed to read access token from JWT", e);
}
}
return this;
}
public AccessToken getToken() throws VerificationException {
parse();
return token;
}
public JWSHeader getHeader() throws VerificationException {
parse();
return jws.getHeader();
}
public TokenVerifier verify() throws VerificationException {
parse();
if (checkRealmUrl && realmUrl == null) {
throw new VerificationException("Realm URL not set");
}
AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
if (AlgorithmType.RSA.equals(algorithmType)) {
if (publicKey == null) {
throw new VerificationException("Public key not set");
}
if (!RSAProvider.verify(jws, publicKey)) {
throw new VerificationException("Invalid token signature");
}
} else if (AlgorithmType.HMAC.equals(algorithmType)) {
if (secretKey == null) {
throw new VerificationException("Secret key not set");
}
if (!HMACProvider.verify(jws, secretKey)) {
throw new VerificationException("Invalid token signature");
}
} else {
throw new VerificationException("Unknown or unsupported token algorith");
}
String user = token.getSubject();
if (user == null) {
throw new VerificationException("Subject missing in token");
}
if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
}
if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
}
if (checkActive && !token.isActive()) {
throw new VerificationException("Token is not active");
}
return this;
}
}

View file

@ -26,23 +26,30 @@ import org.keycloak.jose.jws.crypto.SignatureProvider;
*/
public enum Algorithm {
none(null),
HS256(null),
HS384(null),
HS512(null),
RS256(new RSAProvider()),
RS384(new RSAProvider()),
RS512(new RSAProvider()),
ES256(null),
ES384(null),
ES512(null)
none(null, null),
HS256(AlgorithmType.HMAC, null),
HS384(AlgorithmType.HMAC, null),
HS512(AlgorithmType.HMAC, null),
RS256(AlgorithmType.RSA, new RSAProvider()),
RS384(AlgorithmType.RSA, new RSAProvider()),
RS512(AlgorithmType.RSA, new RSAProvider()),
ES256(AlgorithmType.ECDSA, null),
ES384(AlgorithmType.ECDSA, null),
ES512(AlgorithmType.ECDSA, null)
;
private AlgorithmType type;
private SignatureProvider provider;
Algorithm(SignatureProvider provider) {
Algorithm(AlgorithmType type, SignatureProvider provider) {
this.type = type;
this.provider = provider;
}
public AlgorithmType getType() {
return type;
}
public SignatureProvider getProvider() {
return provider;
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.jose.jws;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public enum AlgorithmType {
RSA,
HMAC,
ECDSA
}

View file

@ -30,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
"resource", "public-client", "credentials",
"use-resource-role-mappings",
"enable-cors", "cors-max-age", "cors-allowed-methods",
"expose-token", "bearer-only",
"expose-token", "bearer-only", "autodetect-bearer-only",
"connection-pool-size",
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
"client-keystore", "client-keystore-password", "client-key-password",

View file

@ -33,7 +33,7 @@ import java.util.Map;
"resource", "public-client", "credentials",
"use-resource-role-mappings",
"enable-cors", "cors-max-age", "cors-allowed-methods",
"expose-token", "bearer-only", "enable-basic-auth"})
"expose-token", "bearer-only", "autodetect-bearer-only", "enable-basic-auth"})
public class BaseAdapterConfig extends BaseRealmConfig {
@JsonProperty("resource")
protected String resource;
@ -51,6 +51,8 @@ public class BaseAdapterConfig extends BaseRealmConfig {
protected boolean exposeToken;
@JsonProperty("bearer-only")
protected boolean bearerOnly;
@JsonProperty("autodetect-bearer-only")
protected boolean autodetectBearerOnly;
@JsonProperty("enable-basic-auth")
protected boolean enableBasicAuth;
@JsonProperty("public-client")
@ -123,6 +125,14 @@ public class BaseAdapterConfig extends BaseRealmConfig {
this.bearerOnly = bearerOnly;
}
public boolean isAutodetectBearerOnly() {
return autodetectBearerOnly;
}
public void setAutodetectBearerOnly(boolean autodetectBearerOnly) {
this.autodetectBearerOnly = autodetectBearerOnly;
}
public boolean isEnableBasicAuth() {
return enableBasicAuth;
}

View file

@ -112,7 +112,7 @@ public class PolicyEnforcerConfig {
public void setOnDenyRedirectTo(String onDenyRedirectTo) {
this.onDenyRedirectTo = onDenyRedirectTo;
}
public static class PathConfig {
private String name;

View file

@ -54,6 +54,8 @@ public class RealmRepresentation {
protected Boolean registrationEmailAsUsername;
protected Boolean rememberMe;
protected Boolean verifyEmail;
protected Boolean loginWithEmailAllowed;
protected Boolean duplicateEmailsAllowed;
protected Boolean resetPasswordAllowed;
protected Boolean editUsernameAllowed;
@ -418,6 +420,22 @@ public class RealmRepresentation {
public void setVerifyEmail(Boolean verifyEmail) {
this.verifyEmail = verifyEmail;
}
public Boolean isLoginWithEmailAllowed() {
return loginWithEmailAllowed;
}
public void setLoginWithEmailAllowed(Boolean loginWithEmailAllowed) {
this.loginWithEmailAllowed = loginWithEmailAllowed;
}
public Boolean isDuplicateEmailsAllowed() {
return duplicateEmailsAllowed;
}
public void setDuplicateEmailsAllowed(Boolean duplicateEmailsAllowed) {
this.duplicateEmailsAllowed = duplicateEmailsAllowed;
}
public Boolean isResetPasswordAllowed() {
return resetPasswordAllowed;

View file

@ -16,11 +16,7 @@
*/
package org.keycloak.representations.idm.authorization;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -36,9 +32,6 @@ public class PolicyRepresentation {
private Logic logic = Logic.POSITIVE;
private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS;
private Map<String, String> config = new HashMap();
private List<PolicyRepresentation> dependentPolicies;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<PolicyRepresentation> associatedPolicies = new ArrayList<>();
public String getId() {
return this.id;
@ -96,14 +89,6 @@ public class PolicyRepresentation {
this.description = description;
}
public List<PolicyRepresentation> getAssociatedPolicies() {
return associatedPolicies;
}
public void setAssociatedPolicies(List<PolicyRepresentation> associatedPolicies) {
this.associatedPolicies = associatedPolicies;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
@ -116,12 +101,4 @@ public class PolicyRepresentation {
public int hashCode() {
return Objects.hash(getId());
}
public void setDependentPolicies(List<PolicyRepresentation> dependentPolicies) {
this.dependentPolicies = dependentPolicies;
}
public List<PolicyRepresentation> getDependentPolicies() {
return this.dependentPolicies;
}
}

View file

@ -16,14 +16,14 @@
*/
package org.keycloak.representations.idm.authorization;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* <p>One or more resources that the resource server manages as a set of protected resources.
@ -159,18 +159,6 @@ public class ResourceRepresentation {
this.owner = owner;
}
public List<PolicyRepresentation> getPolicies() {
return this.policies;
}
public void setPolicies(List<PolicyRepresentation> policies) {
this.policies = policies;
}
<T> T test(Predicate<T> t) {
return null;
}
public void setTypedScopes(List<ScopeRepresentation> typedScopes) {
this.typedScopes = typedScopes;
}
@ -178,4 +166,15 @@ public class ResourceRepresentation {
public List<ScopeRepresentation> getTypedScopes() {
return typedScopes;
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ResourceRepresentation scope = (ResourceRepresentation) o;
return Objects.equals(getName(), scope.getName());
}
public int hashCode() {
return Objects.hash(getName());
}
}

View file

@ -19,18 +19,26 @@ package org.keycloak.representations.info;
import org.keycloak.common.Profile;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ProfileInfoRepresentation {
private String name;
private boolean previewEnabled;
private List<String> disabledFeatures;
public static ProfileInfoRepresentation create() {
ProfileInfoRepresentation info = new ProfileInfoRepresentation();
info.setName(Profile.getName());
info.setPreviewEnabled(Profile.isPreviewEnabled());
info.name = Profile.getName();
info.disabledFeatures = new LinkedList<>();
for (Profile.Feature f : Profile.getDisabledFeatures()) {
info.disabledFeatures.add(f.name());
}
return info;
}
@ -38,16 +46,8 @@ public class ProfileInfoRepresentation {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isPreviewEnabled() {
return previewEnabled;
}
public void setPreviewEnabled(boolean previewEnabled) {
this.previewEnabled = previewEnabled;
public List<String> getDisabledFeatures() {
return disabledFeatures;
}
}

View file

@ -13,16 +13,8 @@
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>All Resources</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
<role-name>user_premium</role-name>
</auth-constraint>
</security-constraint>
@ -39,9 +31,13 @@
<role-name>user</role-name>
</security-role>
<security-role>
<role-name>user_premium</role-name>
</security-role>
<error-page>
<error-code>403</error-code>
<location>/accessDenied.jsp</location>
</error-page>
</web-app>
</web-app>

View file

@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
<name>Examples</name>
<name>Demo Examples</name>
<description/>
<modelVersion>4.0.0</modelVersion>

View file

@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
<name>Fuse examples</name>
<name>Fuse Examples</name>
<description/>
<modelVersion>4.0.0</modelVersion>

File diff suppressed because one or more lines are too long

View file

@ -119,6 +119,21 @@
"id.token.claim" : "true",
"access.token.claim" : "true"
}
},
{
"protocolMapper" : "oidc-usermodel-attribute-mapper",
"protocol" : "openid-connect",
"name" : "picture",
"consentText" : "Picture",
"consentRequired" : true,
"config" : {
"Claim JSON Type" : "String",
"user.attribute" : "picture",
"claim.name" : "picture",
"multivalued": "false",
"id.token.claim" : "true",
"access.token.claim" : "true"
}
}
]
}
@ -247,6 +262,19 @@
"always.read.value.from.ldap" : "false"
}
},
{
"name" : "picture",
"federationMapperType" : "user-attribute-ldap-mapper",
"federationProviderDisplayName" : "ldap-apacheds",
"config" : {
"ldap.attribute" : "jpegPhoto",
"user.model.attribute" : "picture",
"is.mandatory.in.ldap" : "false",
"read.only" : "false",
"always.read.value.from.ldap" : "true",
"is.binary.attribute" : "true"
}
},
{
"name" : "realm roles",
"federationMapperType" : "role-ldap-mapper",

View file

@ -0,0 +1,58 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.example.ldap;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.common.util.Base64;
import org.keycloak.representations.IDToken;
/**
* Tests binary LDAP attribute
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPPictureServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("image/jpeg");
ServletOutputStream outputStream = resp.getOutputStream();
KeycloakSecurityContext securityContext = (KeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
IDToken idToken = securityContext.getIdToken();
String profilePicture = idToken.getPicture();
if (profilePicture != null) {
byte[] decodedPicture = Base64.decode(profilePicture);
outputStream.write(decodedPicture);
}
outputStream.flush();
}
}

View file

@ -23,6 +23,16 @@
<module-name>ldap-portal</module-name>
<servlet>
<servlet-name>Picture</servlet-name>
<servlet-class>org.keycloak.example.ldap.LDAPPictureServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Picture</servlet-name>
<url-pattern>/picture/*</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>LDAPApp</web-resource-name>

View file

@ -37,6 +37,9 @@
<p><b>Full Name: </b><%=idToken.getName()%></p>
<p><b>First: </b><%=idToken.getGivenName()%></p>
<p><b>Last: </b><%=idToken.getFamilyName()%></p>
<% if (idToken.getPicture() != null) { %>
<p><b>Profile picture: </b><img src='/ldap-portal/picture' /></p>
<% } %>
<hr />

View file

@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
<name>Examples</name>
<name>Keycloak Examples</name>
<description/>
<modelVersion>4.0.0</modelVersion>

View file

@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
<name>Authenticator Example</name>
<name>REST Example</name>
<description/>
<modelVersion>4.0.0</modelVersion>

View file

@ -23,7 +23,7 @@
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
<name>Provider Examples</name>
<name>SAML Examples</name>
<description/>
<modelVersion>4.0.0</modelVersion>

View file

@ -24,6 +24,7 @@ import org.keycloak.storage.UserStorageProvider;
import javax.naming.directory.SearchControls;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
@ -34,6 +35,7 @@ import java.util.Set;
public class LDAPConfig {
private final MultivaluedHashMap<String, String> config;
private final Set<String> binaryAttributeNames = new HashSet<>();
public LDAPConfig(MultivaluedHashMap<String, String> config) {
this.config = config;
@ -184,4 +186,39 @@ public class LDAPConfig {
return UserStorageProvider.EditMode.valueOf(editModeString);
}
}
public void addBinaryAttribute(String attrName) {
binaryAttributeNames.add(attrName);
}
public Set<String> getBinaryAttributeNames() {
return binaryAttributeNames;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof LDAPConfig)) return false;
LDAPConfig that = (LDAPConfig) obj;
if (!config.equals(that.config)) return false;
if (!binaryAttributeNames.equals(that.binaryAttributeNames)) return false;
return true;
}
@Override
public int hashCode() {
return config.hashCode() * 13 + binaryAttributeNames.hashCode();
}
@Override
public String toString() {
MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(config);
copy.remove(LDAPConstants.BIND_CREDENTIAL);
return new StringBuilder(copy.toString())
.append(", binaryAttributes: ").append(binaryAttributeNames)
.toString();
}
}

View file

@ -20,8 +20,8 @@ package org.keycloak.storage.ldap;
import org.jboss.logging.Logger;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -33,37 +33,40 @@ public class LDAPIdentityStoreRegistry {
private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class);
private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<String, LDAPIdentityStoreContext>();
private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<>();
public LDAPIdentityStore getLdapStore(ComponentModel model) {
LDAPIdentityStoreContext context = ldapStores.get(model.getId());
public LDAPIdentityStore getLdapStore(ComponentModel ldapModel, Map<ComponentModel, LDAPConfigDecorator> configDecorators) {
LDAPIdentityStoreContext context = ldapStores.get(ldapModel.getId());
// Ldap config might have changed for the realm. In this case, we must re-initialize
MultivaluedHashMap<String, String> config = model.getConfig();
if (context == null || !config.equals(context.config)) {
logLDAPConfig(model.getName(), config);
MultivaluedHashMap<String, String> configModel = ldapModel.getConfig();
LDAPConfig ldapConfig = new LDAPConfig(configModel);
for (Map.Entry<ComponentModel, LDAPConfigDecorator> entry : configDecorators.entrySet()) {
ComponentModel mapperModel = entry.getKey();
LDAPConfigDecorator decorator = entry.getValue();
LDAPIdentityStore store = createLdapIdentityStore(config);
context = new LDAPIdentityStoreContext(config, store);
ldapStores.put(model.getId(), context);
decorator.updateLDAPConfig(ldapConfig, mapperModel);
}
if (context == null || !ldapConfig.equals(context.config)) {
logLDAPConfig(ldapModel.getName(), ldapConfig);
LDAPIdentityStore store = createLdapIdentityStore(ldapConfig);
context = new LDAPIdentityStoreContext(ldapConfig, store);
ldapStores.put(ldapModel.getId(), context);
}
return context.store;
}
// Don't log LDAP password
private void logLDAPConfig(String fedProviderDisplayName, MultivaluedHashMap<String, String> ldapConfig) {
MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(ldapConfig);
copy.remove(LDAPConstants.BIND_CREDENTIAL);
logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderDisplayName + ", LDAP Configuration: " + copy);
private void logLDAPConfig(String fedProviderDisplayName, LDAPConfig ldapConfig) {
logger.infof("Creating new LDAP Store for the LDAP storage provider: '%s', LDAP Configuration: %s", fedProviderDisplayName, ldapConfig.toString());
}
/**
* @param ldapConfig from realm
* @return PartitionManager instance based on LDAP store
* Create LDAPIdentityStore to be cached in the local registry
*/
public static LDAPIdentityStore createLdapIdentityStore(MultivaluedHashMap<String, String> ldapConfig) {
LDAPConfig cfg = new LDAPConfig(ldapConfig);
public static LDAPIdentityStore createLdapIdentityStore(LDAPConfig cfg) {
checkSystemProperty("com.sun.jndi.ldap.connect.pool.authentication", "none simple");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.initsize", "1");
checkSystemProperty("com.sun.jndi.ldap.connect.pool.maxsize", "1000");
@ -84,12 +87,12 @@ public class LDAPIdentityStoreRegistry {
private class LDAPIdentityStoreContext {
private LDAPIdentityStoreContext(MultivaluedHashMap<String, String> config, LDAPIdentityStore store) {
private LDAPIdentityStoreContext(LDAPConfig config, LDAPIdentityStore store) {
this.config = config;
this.store = store;
}
private MultivaluedHashMap<String, String> config;
private LDAPConfig config;
private LDAPIdentityStore store;
}
}

View file

@ -39,6 +39,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.cache.UserCache;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@ -138,6 +139,9 @@ public class LDAPStorageProvider implements UserStorageProvider,
protected UserModel proxy(RealmModel realm, UserModel local, LDAPObject ldapObject) {
UserModel proxied = local;
checkDNChanged(realm, local, ldapObject);
switch (editMode) {
case READ_ONLY:
proxied = new ReadonlyLDAPUserModelDelegate(local, this);
@ -159,6 +163,20 @@ public class LDAPStorageProvider implements UserStorageProvider,
return proxied;
}
private void checkDNChanged(RealmModel realm, UserModel local, LDAPObject ldapObject) {
String dnFromDB = local.getFirstAttribute(LDAPConstants.LDAP_ENTRY_DN);
String ldapDn = ldapObject.getDn().toString();
if (!ldapDn.equals(dnFromDB)) {
logger.debugf("Updated LDAP DN of user '%s' to '%s'", local.getUsername(), ldapDn);
local.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapDn);
UserCache userCache = session.userCache();
if (userCache != null) {
userCache.evict(realm, local);
}
}
}
@Override
public boolean supportsCredentialAuthenticationFor(String type) {
return type.equals(CredentialModel.KERBEROS) && kerberosConfig.isAllowKerberosAuthentication();

View file

@ -48,7 +48,9 @@ import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory;
@ -57,7 +59,9 @@ import org.keycloak.storage.user.SynchronizationResult;
import org.keycloak.utils.CredentialHelper;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -179,10 +183,30 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
@Override
public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
Map<ComponentModel, LDAPConfigDecorator> configDecorators = getLDAPConfigDecorators(session, model);
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model, configDecorators);
return new LDAPStorageProvider(this, session, model, ldapIdentityStore);
}
// Check if it's some performance overhead to create this map in every request. But probably not...
protected Map<ComponentModel, LDAPConfigDecorator> getLDAPConfigDecorators(KeycloakSession session, ComponentModel ldapModel) {
RealmModel realm = session.realms().getRealm(ldapModel.getParentId());
List<ComponentModel> mapperComponents = realm.getComponents(ldapModel.getId(), LDAPStorageMapper.class.getName());
Map<ComponentModel, LDAPConfigDecorator> result = new HashMap<>();
for (ComponentModel mapperModel : mapperComponents) {
LDAPStorageMapperFactory mapperFactory = (LDAPStorageMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(LDAPStorageMapper.class, mapperModel.getProviderId());
if (mapperFactory instanceof LDAPConfigDecorator) {
result.put(mapperModel, (LDAPConfigDecorator) mapperFactory);
}
}
return result;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
LDAPConfig cfg = new LDAPConfig(config.getConfig());

View file

@ -153,11 +153,12 @@ public class LDAPUtils {
* @param ldapProvider
* @param membershipType how is 'member' attribute saved (full DN or just uid)
* @param memberAttrName usually 'member'
* @param memberChildAttrName used just if membershipType is UID. Usually 'uid'
* @param ldapParent role or group
* @param ldapChild usually user (or child group or child role)
* @param sendLDAPUpdateRequest if true, the method will send LDAP update request too. Otherwise it will skip it
*/
public static void addMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
public static void addMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild, boolean sendLDAPUpdateRequest) {
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
@ -171,7 +172,7 @@ public class LDAPUtils {
}
}
String membership = getMemberValueOfChildObject(ldapChild, membershipType);
String membership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
memberships.add(membership);
ldapParent.setAttribute(memberAttrName, memberships);
@ -187,13 +188,14 @@ public class LDAPUtils {
* @param ldapProvider
* @param membershipType how is 'member' attribute saved (full DN or just uid)
* @param memberAttrName usually 'member'
* @param memberChildAttrName used just if membershipType is UID. Usually 'uid'
* @param ldapParent role or group
* @param ldapChild usually user (or child group or child role)
*/
public static void deleteMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
public static void deleteMember(LDAPStorageProvider ldapProvider, MembershipType membershipType, String memberAttrName, String memberChildAttrName, LDAPObject ldapParent, LDAPObject ldapChild) {
Set<String> memberships = getExistingMemberships(memberAttrName, ldapParent);
String userMembership = getMemberValueOfChildObject(ldapChild, membershipType);
String userMembership = getMemberValueOfChildObject(ldapChild, membershipType, memberChildAttrName);
memberships.remove(userMembership);
@ -222,10 +224,14 @@ public class LDAPUtils {
}
/**
* Get value to be used as attribute 'member' in some parent ldapObject
* Get value to be used as attribute 'member' or 'memberUid' in some parent ldapObject
*/
public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType) {
return membershipType == MembershipType.DN ? ldapUser.getDn().toString() : ldapUser.getAttributeAsString(ldapUser.getRdnAttributeName());
public static String getMemberValueOfChildObject(LDAPObject ldapUser, MembershipType membershipType, String memberChildAttrName) {
if (membershipType == MembershipType.DN) {
return ldapUser.getDn().toString();
} else {
return ldapUser.getAttributeAsString(memberChildAttrName);
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.storage.ldap.idm.store.ldap;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Base64;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
@ -40,6 +41,8 @@ import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -323,8 +326,15 @@ public class LDAPIdentityStore implements IdentityStore {
Set<String> attrValues = new LinkedHashSet<>();
NamingEnumeration<?> enumm = ldapAttribute.getAll();
while (enumm.hasMoreElements()) {
String attrVal = enumm.next().toString().trim();
attrValues.add(attrVal);
Object val = enumm.next();
if (val instanceof byte[]) { // byte[]
String attrVal = Base64.encodeBytes((byte[]) val);
attrValues.add(attrVal);
} else { // String
String attrVal = val.toString().trim();
attrValues.add(attrVal);
}
}
if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
@ -377,7 +387,18 @@ public class LDAPIdentityStore implements IdentityStore {
if (val == null || val.toString().trim().length() == 0) {
val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
}
attr.add(val);
if (getConfig().getBinaryAttributeNames().contains(attrName)) {
// Binary attribute
try {
byte[] bytes = Base64.decode(val);
attr.add(bytes);
} catch (IOException ioe) {
logger.warnf("Wasn't able to Base64 decode the attribute value. Ignoring attribute update. LDAP DN: %s, Attribute: %s, Attribute value: %s" + ldapObject.getDn(), attrName, attrValue);
}
} else {
attr.add(val);
}
}
entryAttributes.put(attr);

View file

@ -515,8 +515,17 @@ public class LDAPOperationManager {
}
}
StringBuilder binaryAttrsBuilder = new StringBuilder();
if (this.config.isObjectGUID()) {
env.put("java.naming.ldap.attributes.binary", LDAPConstants.OBJECT_GUID);
binaryAttrsBuilder.append(LDAPConstants.OBJECT_GUID).append(" ");
}
for (String attrName : config.getBinaryAttributeNames()) {
binaryAttrsBuilder.append(attrName).append(" ");
}
String binaryAttrs = binaryAttrsBuilder.toString().trim();
if (!binaryAttrs.isEmpty()) {
env.put("java.naming.ldap.attributes.binary", binaryAttrs);
}
if (logger.isDebugEnabled()) {

View file

@ -22,6 +22,7 @@ import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;

View file

@ -0,0 +1,31 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.storage.ldap.mappers;
import org.keycloak.component.ComponentModel;
import org.keycloak.storage.ldap.LDAPConfig;
/**
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface LDAPConfigDecorator {
void updateLDAPConfig(LDAPConfig ldapConfig, ComponentModel mapperModel);
}

View file

@ -80,6 +80,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
public static final String READ_ONLY = "read.only";
public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
public static final String IS_BINARY_ATTRIBUTE = "is.binary.attribute";
public UserAttributeLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
super(mapperModel, ldapProvider);
@ -90,6 +91,12 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
// We won't update binary attributes to Keycloak DB. They might be too big
boolean isBinaryAttribute = mapperModel.get(IS_BINARY_ATTRIBUTE, false);
if (isBinaryAttribute) {
return;
}
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
if (userModelProperty != null) {
@ -157,7 +164,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
// throw ModelDuplicateException if there is different user in model with same email
protected void checkDuplicateEmail(String userModelAttrName, String email, RealmModel realm, KeycloakSession session, UserModel user) {
if (email == null) return;
if (email == null || realm.isDuplicateEmailsAllowed()) return;
if (UserModel.EMAIL.equalsIgnoreCase(userModelAttrName)) {
// lowercase before search
email = KeycloakModelUtils.toLowerCaseSafe(email);
@ -177,6 +184,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
final String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
final boolean isBinaryAttribute = parseBooleanParameter(mapperModel, IS_BINARY_ATTRIBUTE);
// For writable mode, we want to propagate writing of attribute to LDAP as well
if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE && !isReadOnly()) {
@ -185,20 +193,23 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
@Override
public void setSingleAttribute(String name, String value) {
setLDAPAttribute(name, value);
super.setSingleAttribute(name, value);
if (setLDAPAttribute(name, value)) {
super.setSingleAttribute(name, value);
}
}
@Override
public void setAttribute(String name, List<String> values) {
setLDAPAttribute(name, values);
super.setAttribute(name, values);
if (setLDAPAttribute(name, values)) {
super.setAttribute(name, values);
}
}
@Override
public void removeAttribute(String name) {
setLDAPAttribute(name, null);
super.removeAttribute(name);
if ( setLDAPAttribute(name, null)) {
super.removeAttribute(name);
}
}
@Override
@ -221,10 +232,10 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
super.setFirstName(firstName);
}
protected void setLDAPAttribute(String modelAttrName, Object value) {
protected boolean setLDAPAttribute(String modelAttrName, Object value) {
if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
if (logger.isTraceEnabled()) {
logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
if (UserAttributeLDAPStorageMapper.logger.isTraceEnabled()) {
UserAttributeLDAPStorageMapper.logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
}
ensureTransactionStarted();
@ -245,7 +256,53 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList));
}
}
if (isBinaryAttribute) {
UserAttributeLDAPStorageMapper.logger.debugf("Skip writing model attribute '%s' to DB for user '%s' as it is mapped to binary LDAP attribute.", userModelAttrName, getUsername());
return false;
} else {
return true;
}
}
return true;
}
};
} else if (isBinaryAttribute) {
delegate = new UserModelDelegate(delegate) {
@Override
public void setSingleAttribute(String name, String value) {
if (name.equalsIgnoreCase(userModelAttrName)) {
logSkipDBWrite();
} else {
super.setSingleAttribute(name, value);
}
}
@Override
public void setAttribute(String name, List<String> values) {
if (name.equalsIgnoreCase(userModelAttrName)) {
logSkipDBWrite();
} else {
super.setAttribute(name, values);
}
}
@Override
public void removeAttribute(String name) {
if (name.equalsIgnoreCase(userModelAttrName)) {
logSkipDBWrite();
} else {
super.removeAttribute(name);
}
}
private void logSkipDBWrite() {
logger.debugf("Skip writing model attribute '%s' to DB for user '%s' as it is mapped to binary LDAP attribute", userModelAttrName, getUsername());
}
};

View file

@ -32,7 +32,7 @@ import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory implements LDAPConfigDecorator {
public static final String PROVIDER_ID = "user-attribute-ldap-mapper";
protected static final List<ProviderConfigProperty> configProperties;
@ -69,6 +69,10 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
.helpText("If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("false").add()
.property().name(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE).label("Is Binary Attribute")
.helpText("Should be true for binary LDAP attributes")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue("false").add()
.build();
}
@ -92,6 +96,12 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", config);
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, "LDAP Attribute", config);
boolean isBinaryAttribute = config.get(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, false);
boolean alwaysReadValueFromLDAP = config.get(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, false);
if (isBinaryAttribute && !alwaysReadValueFromLDAP) {
throw new ComponentValidationException("With Binary attribute enabled, the ''Always read value from LDAP'' must be enabled too");
}
}
@Override
@ -103,4 +113,14 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
return getConfigProps(parent);
}
@Override
public void updateLDAPConfig(LDAPConfig ldapConfig, ComponentModel mapperModel) {
boolean isBinaryAttribute = mapperModel.get(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, false);
if (isBinaryAttribute) {
String ldapAttrName = mapperModel.getConfig().getFirst(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE);
ldapConfig.addBinaryAttribute(ldapAttrName);
}
}
}

View file

@ -20,6 +20,7 @@ package org.keycloak.storage.ldap.mappers.membership;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import java.util.HashSet;
import java.util.Set;
@ -35,6 +36,9 @@ public abstract class CommonLDAPGroupMapperConfig {
// See docs for MembershipType enum
public static final String MEMBERSHIP_ATTRIBUTE_TYPE = "membership.attribute.type";
// Used just for membershipType=UID. Name of LDAP attribute on user, which is used for membership mappings. Usually it will be "uid"
public static final String MEMBERSHIP_USER_LDAP_ATTRIBUTE = "membership.user.ldap.attribute";
// See docs for Mode enum
public static final String MODE = "mode";
@ -58,6 +62,11 @@ public abstract class CommonLDAPGroupMapperConfig {
return (membershipType!=null && !membershipType.isEmpty()) ? Enum.valueOf(MembershipType.class, membershipType) : MembershipType.DN;
}
public String getMembershipUserLdapAttribute(LDAPConfig ldapConfig) {
String membershipUserAttrName = mapperModel.getConfig().getFirst(MEMBERSHIP_USER_LDAP_ATTRIBUTE);
return membershipUserAttrName!=null ? membershipUserAttrName : ldapConfig.getUsernameLdapAttribute();
}
public LDAPGroupMapperMode getMode() {
String modeString = mapperModel.getConfig().getFirst(MODE);
if (modeString == null || modeString.isEmpty()) {

View file

@ -57,7 +57,7 @@ public enum MembershipType {
protected Set<LDAPDn> getLDAPMembersWithParent(LDAPObject ldapGroup, String membershipLdapAttribute, LDAPDn requiredParentDn) {
Set<String> allMemberships = LDAPUtils.getExistingMemberships(membershipLdapAttribute, ldapGroup);
// Filter and keep just groups
// Filter and keep just descendants of requiredParentDn
Set<LDAPDn> result = new HashSet<>();
for (String membership : allMemberships) {
LDAPDn childDn = LDAPDn.fromString(membership);
@ -135,6 +135,9 @@ public enum MembershipType {
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupLDAPStorageMapper groupMapper, LDAPObject ldapGroup, int firstResult, int maxResults) {
LDAPStorageProvider ldapProvider = groupMapper.getLdapProvider();
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
String memberAttrName = groupMapper.getConfig().getMembershipLdapAttribute();
Set<String> memberUids = LDAPUtils.getExistingMemberships(memberAttrName, ldapGroup);
@ -146,7 +149,34 @@ public enum MembershipType {
int max = Math.min(memberUids.size(), firstResult + maxResults);
uids = uids.subList(firstResult, max);
return groupMapper.getLdapProvider().loadUsersByUsernames(uids, realm);
String membershipUserAttrName = groupMapper.getConfig().getMembershipUserLdapAttribute(ldapConfig);
List<String> usernames;
if (membershipUserAttrName.equals(ldapConfig.getUsernameLdapAttribute())) {
usernames = uids; // Optimized version. No need to
} else {
usernames = new LinkedList<>();
LDAPQuery query = LDAPUtils.createQueryForUserSearch(ldapProvider, realm);
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition[] orSubconditions = new Condition[uids.size()];
int index = 0;
for (String memberUid : uids) {
Condition condition = conditionsBuilder.equal(membershipUserAttrName, memberUid, EscapeStrategy.DEFAULT);
orSubconditions[index] = condition;
index++;
}
Condition orCondition = conditionsBuilder.orCondition(orSubconditions);
query.addWhereCondition(orCondition);
List<LDAPObject> ldapUsers = query.getResultList();
for (LDAPObject ldapUser : ldapUsers) {
String username = LDAPUtils.getUsername(ldapUser, ldapConfig);
usernames.add(username);
}
}
return groupMapper.getLdapProvider().loadUsersByUsernames(usernames, realm);
}
};

View file

@ -19,6 +19,7 @@ package org.keycloak.storage.ldap.mappers.membership;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@ -39,7 +40,7 @@ import java.util.Set;
public interface UserRolesRetrieveStrategy {
List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser);
List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser, LDAPConfig ldapConfig);
void beforeUserLDAPQuery(LDAPQuery query);
@ -52,11 +53,12 @@ public interface UserRolesRetrieveStrategy {
class LoadRolesByMember implements UserRolesRetrieveStrategy {
@Override
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser, LDAPConfig ldapConfig) {
LDAPQuery ldapQuery = roleOrGroupMapper.createLDAPGroupQuery();
String membershipAttr = roleOrGroupMapper.getConfig().getMembershipLdapAttribute();
String userMembership = LDAPUtils.getMemberValueOfChildObject(ldapUser, roleOrGroupMapper.getConfig().getMembershipTypeLdapAttribute());
String membershipUserAttrName = roleOrGroupMapper.getConfig().getMembershipUserLdapAttribute(ldapConfig);
String userMembership = LDAPUtils.getMemberValueOfChildObject(ldapUser, roleOrGroupMapper.getConfig().getMembershipTypeLdapAttribute(), membershipUserAttrName);
Condition membershipCondition = getMembershipCondition(membershipAttr, userMembership);
ldapQuery.addWhereCondition(membershipCondition);
@ -79,7 +81,7 @@ public interface UserRolesRetrieveStrategy {
class GetRolesFromUserMemberOfAttribute implements UserRolesRetrieveStrategy {
@Override
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser) {
public List<LDAPObject> getLDAPRoleMappings(CommonLDAPGroupMapper roleOrGroupMapper, LDAPObject ldapUser, LDAPConfig ldapConfig) {
Set<String> memberOfValues = ldapUser.getAttributeAsSet(LDAPConstants.MEMBER_OF);
if (memberOfValues == null) {
return Collections.emptyList();

View file

@ -27,6 +27,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
@ -450,11 +451,13 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
LDAPObject ldapGroup = ldapGroupsMap.get(kcGroup.getName());
Set<LDAPDn> toRemoveSubgroupsDNs = getLDAPSubgroups(ldapGroup);
String membershipUserLdapAttrName = getMembershipUserLdapAttribute(); // Not applicable for groups, but needs to be here
// Add LDAP subgroups, which are KC subgroups
Set<GroupModel> kcSubgroups = kcGroup.getSubGroups();
for (GroupModel kcSubgroup : kcSubgroups) {
LDAPObject ldapSubgroup = ldapGroupsMap.get(kcSubgroup.getName());
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, ldapSubgroup, false);
LDAPUtils.addMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapSubgroup, false);
toRemoveSubgroupsDNs.remove(ldapSubgroup.getDn());
}
@ -462,7 +465,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
for (LDAPDn toRemoveDN : toRemoveSubgroupsDNs) {
LDAPObject fakeGroup = new LDAPObject();
fakeGroup.setDn(toRemoveDN);
LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), ldapGroup, fakeGroup);
LDAPUtils.deleteMember(ldapProvider, MembershipType.DN, config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, fakeGroup);
}
// Update group to LDAP
@ -497,17 +500,22 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
ldapGroup = loadLDAPGroupByName(groupName);
}
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser, true);
String membershipUserLdapAttrName = getMembershipUserLdapAttribute();
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapUser, true);
}
public void deleteGroupMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapGroup) {
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapGroup, ldapUser);
String membershipUserLdapAttrName = getMembershipUserLdapAttribute();
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserLdapAttrName, ldapGroup, ldapUser);
}
protected List<LDAPObject> getLDAPGroupMappings(LDAPObject ldapUser) {
String strategyKey = config.getUserGroupsRetrieveStrategy();
UserRolesRetrieveStrategy strategy = factory.getUserGroupsRetrieveStrategy(strategyKey);
return strategy.getLDAPRoleMappings(this, ldapUser);
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
return strategy.getLDAPRoleMappings(this, ldapUser, ldapConfig);
}
@Override
@ -555,6 +563,12 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
}
protected String getMembershipUserLdapAttribute() {
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
return config.getMembershipUserLdapAttribute(ldapConfig);
}
public class LDAPGroupMappingsUserDelegate extends UserModelDelegate {
private final RealmModel realm;
@ -604,8 +618,11 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
LDAPQuery ldapQuery = createGroupQuery();
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(config.getGroupNameLdapAttribute(), group.getName());
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
String membershipUserLdapAttrName = getMembershipUserLdapAttribute();
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute(), membershipUserLdapAttrName);
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
LDAPObject ldapGroup = ldapQuery.getFirstResult();

View file

@ -34,6 +34,7 @@ import org.keycloak.storage.ldap.mappers.membership.CommonLDAPGroupMapperConfig;
import org.keycloak.storage.ldap.mappers.membership.LDAPGroupMapperMode;
import org.keycloak.storage.ldap.mappers.membership.MembershipType;
import org.keycloak.storage.ldap.mappers.membership.UserRolesRetrieveStrategy;
import org.keycloak.storage.ldap.mappers.membership.role.RoleMapperConfig;
import java.util.HashMap;
import java.util.LinkedHashMap;
@ -74,11 +75,14 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
private static List<ProviderConfigProperty> getProps(ComponentModel parent) {
String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
String membershipUserAttribute = LDAPConstants.UID;
if (parent != null) {
LDAPConfig config = new LDAPConfig(parent.getConfig());
roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
mode = config.getEditMode() == UserStorageProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
membershipUserAttribute = config.getUsernameLdapAttribute();
}
return ProviderConfigurationBuilder.create()
.property().name(GroupMapperConfig.GROUPS_DN)
.label("LDAP Groups DN")
@ -106,7 +110,8 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
.add()
.property().name(GroupMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
.label("Membership LDAP Attribute")
.helpText("Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ")
.helpText("Name of LDAP attribute on group, which is used for membership mappings. Usually it will be 'member' ." +
"However when 'Membership Attribute Type' is 'UID' then 'Membership LDAP Attribute' could be typically 'memberUid' .")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(LDAPConstants.MEMBER)
.add()
@ -118,6 +123,14 @@ public class GroupLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFact
.options(MEMBERSHIP_TYPES)
.defaultValue(MembershipType.DN.toString())
.add()
.property().name(RoleMapperConfig.MEMBERSHIP_USER_LDAP_ATTRIBUTE)
.label("Membership User LDAP Attribute")
.helpText("Used just if Membership Attribute Type is UID. It is name of LDAP attribute on user, which is used for membership mappings. Usually it will be 'uid' . For example if value of " +
"'Membership User LDAP Attribute' is 'uid' and " +
" LDAP group has 'memberUid: john', then it is expected that particular LDAP user will have attribute 'uid: john' .")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(membershipUserAttribute)
.add()
.property().name(GroupMapperConfig.GROUPS_LDAP_FILTER)
.label("LDAP Filter")
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP groups. Leave this empty if no additional filtering is needed and you want to retrieve all groups from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")

View file

@ -90,11 +90,6 @@ public class GroupMapperConfig extends CommonLDAPGroupMapperConfig {
return AbstractLDAPStorageMapper.parseBooleanParameter(mapperModel, PRESERVE_GROUP_INHERITANCE);
}
public String getMembershipLdapAttribute() {
String membershipAttrName = mapperModel.getConfig().getFirst(MEMBERSHIP_LDAP_ATTRIBUTE);
return membershipAttrName!=null ? membershipAttrName : LDAPConstants.MEMBER;
}
public Collection<String> getGroupObjectClasses(LDAPStorageProvider ldapProvider) {
String objectClasses = mapperModel.getConfig().getFirst(GROUP_OBJECT_CLASSES);
if (objectClasses == null) {

View file

@ -27,6 +27,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
@ -252,11 +253,14 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
ldapRole = createLDAPRole(roleName);
}
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser, true);
String membershipUserAttrName = getMembershipUserLdapAttribute();
LDAPUtils.addMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserAttrName, ldapRole, ldapUser, true);
}
public void deleteRoleMappingInLDAP(LDAPObject ldapUser, LDAPObject ldapRole) {
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), ldapRole, ldapUser);
String membershipUserAttrName = getMembershipUserLdapAttribute();
LDAPUtils.deleteMember(ldapProvider, config.getMembershipTypeLdapAttribute(), config.getMembershipLdapAttribute(), membershipUserAttrName, ldapRole, ldapUser);
}
public LDAPObject loadLDAPRoleByName(String roleName) {
@ -269,7 +273,9 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
protected List<LDAPObject> getLDAPRoleMappings(LDAPObject ldapUser) {
String strategyKey = config.getUserRolesRetrieveStrategy();
UserRolesRetrieveStrategy strategy = factory.getUserRolesRetrieveStrategy(strategyKey);
return strategy.getLDAPRoleMappings(this, ldapUser);
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
return strategy.getLDAPRoleMappings(this, ldapUser, ldapConfig);
}
@Override
@ -292,6 +298,11 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
}
protected String getMembershipUserLdapAttribute() {
LDAPConfig ldapConfig = ldapProvider.getLdapIdentityStore().getConfig();
return config.getMembershipUserLdapAttribute(ldapConfig);
}
public class LDAPRoleMappingsUserDelegate extends UserModelDelegate {
@ -422,8 +433,12 @@ public class RoleLDAPStorageMapper extends AbstractLDAPStorageMapper implements
LDAPQuery ldapQuery = createRoleQuery();
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(config.getRoleNameLdapAttribute(), role.getName());
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute());
String membershipUserAttrName = getMembershipUserLdapAttribute();
String membershipUserAttr = LDAPUtils.getMemberValueOfChildObject(ldapUser, config.getMembershipTypeLdapAttribute(), membershipUserAttrName);
Condition membershipCondition = conditionsBuilder.equal(config.getMembershipLdapAttribute(), membershipUserAttr);
ldapQuery.addWhereCondition(roleNameCondition).addWhereCondition(membershipCondition);
LDAPObject ldapRole = ldapQuery.getFirstResult();

View file

@ -74,11 +74,14 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
private static List<ProviderConfigProperty> getProps(ComponentModel parent) {
String roleObjectClasses = LDAPConstants.GROUP_OF_NAMES;
String mode = LDAPGroupMapperMode.LDAP_ONLY.toString();
String membershipUserAttribute = LDAPConstants.UID;
if (parent != null) {
LDAPConfig config = new LDAPConfig(parent.getConfig());
roleObjectClasses = config.isActiveDirectory() ? LDAPConstants.GROUP : LDAPConstants.GROUP_OF_NAMES;
mode = config.getEditMode() == UserStorageProvider.EditMode.WRITABLE ? LDAPGroupMapperMode.LDAP_ONLY.toString() : LDAPGroupMapperMode.READ_ONLY.toString();
membershipUserAttribute = config.getUsernameLdapAttribute();
}
return ProviderConfigurationBuilder.create()
.property().name(RoleMapperConfig.ROLES_DN)
.label("LDAP Roles DN")
@ -99,7 +102,8 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
.add()
.property().name(RoleMapperConfig.MEMBERSHIP_LDAP_ATTRIBUTE)
.label("Membership LDAP Attribute")
.helpText("Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ")
.helpText("Name of LDAP attribute on role, which is used for membership mappings. Usually it will be 'member' ." +
"However when 'Membership Attribute Type' is 'UID' then 'Membership LDAP Attribute' could be typically 'memberUid' .")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(LDAPConstants.MEMBER)
.add()
@ -111,6 +115,14 @@ public class RoleLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFacto
.options(MEMBERSHIP_TYPES)
.defaultValue(MembershipType.DN.toString())
.add()
.property().name(RoleMapperConfig.MEMBERSHIP_USER_LDAP_ATTRIBUTE)
.label("Membership User LDAP Attribute")
.helpText("Used just if Membership Attribute Type is UID. It is name of LDAP attribute on user, which is used for membership mappings. Usually it will be 'uid' . For example if value of " +
"'Membership User LDAP Attribute' is 'uid' and " +
" LDAP group has 'memberUid: john', then it is expected that particular LDAP user will have attribute 'uid: john' .")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(membershipUserAttribute)
.add()
.property().name(RoleMapperConfig.ROLES_LDAP_FILTER)
.label("LDAP Filter")
.helpText("LDAP Filter adds additional custom filter to the whole query for retrieve LDAP roles. Leave this empty if no additional filtering is needed and you want to retrieve all roles from LDAP. Otherwise make sure that filter starts with '(' and ends with ')'")

View file

@ -21,7 +21,14 @@ package cx.ath.matthew;
*/
public class LibraryLoader {
private static final String[] PATHS = {"/usr/lib/", "/usr/lib64/", "/usr/local/lib/", "/opt/local/lib/"};
private static final String[] PATHS = {
"/opt/rh/rh-sso7/root/lib/",
"/opt/rh/rh-sso7/root/lib64/",
"/usr/lib/",
"/usr/lib64/",
"/usr/local/lib/",
"/opt/local/lib/"
};
private static final String LIBRARY_NAME = "libunix_dbus_java";
private static final String VERSION = "0.0.8";
private static boolean loadSucceeded;

View file

@ -26,7 +26,6 @@
*/
package cx.ath.matthew.unix;
import cx.ath.matthew.LibraryLoader;
import cx.ath.matthew.debug.Debug;
import java.io.IOException;
@ -37,9 +36,6 @@ import java.io.OutputStream;
* Represents a UnixSocket.
*/
public class UnixSocket {
static {
LibraryLoader.load();
}
private native void native_set_pass_cred(int sock, boolean passcred) throws IOException;

View file

@ -34,11 +34,13 @@ public interface InfoPipe extends DBusInterface {
String OBJECTPATH = "/org/freedesktop/sssd/infopipe";
String BUSNAME = "org.freedesktop.sssd.infopipe";
@DBusMemberName("GetUserAttr")
Map<String, Variant> getUserAttributes(String user, List<String> attr);
@DBusMemberName("GetUserGroups")
List<String> getUserGroups(String user);
@DBusMemberName("Ping")
String ping(String ping);
}

View file

@ -17,15 +17,13 @@
package org.keycloak.federation.sssd.api;
import cx.ath.matthew.LibraryLoader;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.Variant;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.sssd.infopipe.InfoPipe;
import org.jboss.logging.Logger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@ -52,7 +50,8 @@ public class Sssd {
public Sssd(String username) {
this.username = username;
try {
dBusConnection = DBusConnection.getConnection(DBusConnection.SYSTEM);
if (LibraryLoader.load().succeed())
dBusConnection = DBusConnection.getConnection(DBusConnection.SYSTEM);
} catch (DBusException e) {
e.printStackTrace();
}
@ -96,14 +95,20 @@ public class Sssd {
public static boolean isAvailable() {
boolean sssdAvailable = false;
try {
Path path = Paths.get("/etc/sssd");
if (!Files.exists(path)) {
logger.debugv("SSSD is not available in your system. Federation provider will be disabled.");
if (LibraryLoader.load().succeed()) {
DBusConnection connection = DBusConnection.getConnection(DBusConnection.SYSTEM);
InfoPipe infoPipe = connection.getRemoteObject(InfoPipe.BUSNAME, InfoPipe.OBJECTPATH, InfoPipe.class);
if (infoPipe.ping("PING") == null || infoPipe.ping("PING").isEmpty()) {
logger.debugv("SSSD is not available in your system. Federation provider will be disabled.");
} else {
sssdAvailable = true;
}
} else {
sssdAvailable = true;
logger.debugv("The RPM libunix-dbus-java is not installed. SSSD Federation provider will be disabled.");
}
} catch (Exception e) {
logger.error("SSSD check failed", e);
logger.debugv("SSSD is not available in your system. Federation provider will be disabled.", e);
}
return sssdAvailable;
}

View file

@ -110,7 +110,7 @@ public class Config {
}
public static void checkGrantType(String grantType) {
if (!PASSWORD.equals(grantType) && !CLIENT_CREDENTIALS.equals(grantType)) {
if (grantType != null && !PASSWORD.equals(grantType) && !CLIENT_CREDENTIALS.equals(grantType)) {
throw new IllegalArgumentException("Unsupported grantType: " + grantType +
" (only " + PASSWORD + " and " + CLIENT_CREDENTIALS + " are supported)");
}

View file

@ -43,18 +43,22 @@ import static org.keycloak.OAuth2Constants.PASSWORD;
public class Keycloak {
private final Config config;
private final TokenManager tokenManager;
private String authToken;
private final ResteasyWebTarget target;
private final ResteasyClient client;
Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient) {
Keycloak(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, String grantType, ResteasyClient resteasyClient, String authtoken) {
config = new Config(serverUrl, realm, username, password, clientId, clientSecret, grantType);
client = resteasyClient != null ? resteasyClient : new ResteasyClientBuilder().connectionPoolSize(10).build();
tokenManager = new TokenManager(config, client);
authToken = authtoken;
tokenManager = authtoken == null ? new TokenManager(config, client) : null;
target = client.target(config.getServerUrl());
target.register(newAuthFilter());
}
target.register(new BearerAuthFilter(tokenManager));
private BearerAuthFilter newAuthFilter() {
return authToken != null ? new BearerAuthFilter(authToken) : new BearerAuthFilter(tokenManager);
}
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) {
@ -63,15 +67,19 @@ public class Keycloak {
.hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.WILDCARD)
.connectionPoolSize(10).build();
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client);
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, client, null);
}
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) {
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null);
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, null, null);
}
public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) {
return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null);
return new Keycloak(serverUrl, realm, username, password, clientId, null, PASSWORD, null, null);
}
public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authtoken) {
return new Keycloak(serverUrl, realm, null, null, clientId, null, PASSWORD, null, null);
}
public RealmsResource realms() {
@ -100,7 +108,7 @@ public class Keycloak {
* @return
*/
public <T> T proxy(Class<T> proxyClass, URI absoluteURI) {
return client.target(absoluteURI).register(new BearerAuthFilter(tokenManager)).proxy(proxyClass);
return client.target(absoluteURI).register(newAuthFilter()).proxy(proxyClass);
}
/**

View file

@ -60,8 +60,9 @@ public class KeycloakBuilder {
private String password;
private String clientId;
private String clientSecret;
private String grantType = PASSWORD;
private String grantType;
private ResteasyClient resteasyClient;
private String authorization;
public KeycloakBuilder serverUrl(String serverUrl) {
this.serverUrl = serverUrl;
@ -104,6 +105,11 @@ public class KeycloakBuilder {
return this;
}
public KeycloakBuilder authorization(String auth) {
this.authorization = auth;
return this;
}
/**
* Builds a new Keycloak client from this builder.
*/
@ -116,6 +122,10 @@ public class KeycloakBuilder {
throw new IllegalStateException("realm required");
}
if (authorization == null && grantType == null) {
grantType = PASSWORD;
}
if (PASSWORD.equals(grantType)) {
if (username == null) {
throw new IllegalStateException("username required");
@ -130,11 +140,11 @@ public class KeycloakBuilder {
}
}
if (clientId == null) {
if (authorization == null && clientId == null) {
throw new IllegalStateException("clientId required");
}
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient);
return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, grantType, resteasyClient, authorization);
}
private KeycloakBuilder() {

View file

@ -49,8 +49,10 @@ public class BearerAuthFilter implements ClientRequestFilter, ClientResponseFilt
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
String authHeader = AUTH_HEADER_PREFIX + (tokenManager != null ? tokenManager.getAccessTokenString() : tokenString);
String authHeader = (tokenManager != null ? tokenManager.getAccessTokenString() : tokenString);
if (!authHeader.startsWith(AUTH_HEADER_PREFIX)) {
authHeader = AUTH_HEADER_PREFIX + authHeader;
}
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
}

View file

@ -16,16 +16,21 @@
*/
package org.keycloak.admin.client.resource;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@ -42,4 +47,28 @@ public interface PolicyResource {
@DELETE
void remove();
@Path("/associatedPolicies")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
List<PolicyRepresentation> associatedPolicies();
@Path("/dependentPolicies")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
List<PolicyRepresentation> dependentPolicies();
@Path("/scopes")
@GET
@Produces("application/json")
@NoCache
List<ScopeRepresentation> scopes();
@Path("/resources")
@GET
@Produces("application/json")
@NoCache
List<ResourceRepresentation> resources();
}

View file

@ -16,13 +16,17 @@
*/
package org.keycloak.admin.client.resource;
import java.util.List;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@ -42,4 +46,10 @@ public interface ResourceResource {
@DELETE
void remove();
@Path("permissions")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
List<PolicyRepresentation> permissions();
}

View file

@ -16,13 +16,17 @@
*/
package org.keycloak.admin.client.resource;
import java.util.List;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@ -42,4 +46,9 @@ public interface ResourceScopeResource {
@DELETE
void remove();
@Path("/permissions")
@GET
@Produces(MediaType.APPLICATION_JSON)
List<PolicyRepresentation> permissions();
}

View file

@ -0,0 +1,177 @@
<?xml version="1.0"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ 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.
-->
<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/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-client-cli-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>2.5.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-admin-cli</artifactId>
<name>Keycloak Admin CLI</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<!--version>2.4.3</version-->
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>org.keycloak:keycloak-core</artifact>
<includes>
<include>org/keycloak/util/**</include>
<include>org/keycloak/json/**</include>
<include>org/keycloak/jose/jws/**</include>
<include>org/keycloak/jose/jwk/**</include>
<include>org/keycloak/representations/adapters/config/**</include>
<include>org/keycloak/representations/adapters/action/**</include>
<include>org/keycloak/representations/AccessTokenResponse.class</include>
<!--
<include>org/keycloak/representations/idm/ClientRepresentation.class</include>
<include>org/keycloak/representations/idm/RealmRepresentation.class</include>
<include>org/keycloak/representations/idm/UserRepresentation.class</include>
<include>org/keycloak/representations/idm/RoleRepresentation.class</include>
<include>org/keycloak/representations/idm/RoleRepresentation.class</include>
<include>org/keycloak/representations/idm/RolesRepresentation.class</include>
<include>org/keycloak/representations/idm/ScopeMappingRepresentation.class</include>
<include>org/keycloak/representations/idm/UserFederationMapperRepresentation.class</include>
<include>org/keycloak/representations/idm/ProtocolMapperRepresentation.class</include>
<include>org/keycloak/representations/idm/IdentityProviderRepresentation.class</include>
<include>org/keycloak/representations/idm/authorization/**</include>
-->
<include>org/keycloak/representations/idm/**</include>
<include>org/keycloak/representations/JsonWebToken.class</include>
</includes>
</filter>
<filter>
<artifact>org.keycloak:keycloak-common</artifact>
<includes>
<include>org/keycloak/common/util/**</include>
</includes>
</filter>
<filter>
<artifact>org.bouncycastle:bcprov-jdk15on</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>org.bouncycastle:bcpkix-jdk15on</artifact>
<excludes>
<exclude>**/**</exclude>
</excludes>
</filter>
<filter>
<artifact>com.fasterxml.jackson.core:jackson-core</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>com.fasterxml.jackson.core:jackson-databind</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>com.fasterxml.jackson.core:jackson-annotations</artifact>
<includes>
<include>com/fasterxml/jackson/annotation/**</include>
</includes>
</filter>
<filter>
<artifact>org.jboss.resteasy:resteasy-client</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>org.jboss.resteasy:resteasy-jaxrs</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>org.jboss.resteasy:resteasy-jackson2-provider</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_2.0_spec</artifact>
<includes>
<include>**/**</include>
</includes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,8 @@
@echo off
if "%OS%" == "Windows_NT" (
set "DIRNAME=%~dp0%"
) else (
set DIRNAME=.\
)
java %KC_OPTS% -cp %DIRNAME%\client\keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain %*

View file

@ -0,0 +1,23 @@
#!/bin/sh
case "`uname`" in
CYGWIN*)
CFILE = `cygpath "$0"`
RESOLVED_NAME=`readlink -f "$CFILE"`
;;
Darwin*)
RESOLVED_NAME=`readlink "$0"`
;;
FreeBSD)
RESOLVED_NAME=`readlink -f "$0"`
;;
Linux)
RESOLVED_NAME=`readlink -f "$0"`
;;
esac
if [ "x$RESOLVED_NAME" = "x" ]; then
RESOLVED_NAME="$0"
fi
DIRNAME=`dirname "$RESOLVED_NAME"`
java $KC_OPTS -cp $DIRNAME/client/keycloak-admin-cli-${project.version}.jar org.keycloak.client.admin.cli.KcAdmMain "$@"

View file

@ -0,0 +1,94 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli;
import org.jboss.aesh.console.AeshConsoleBuilder;
import org.jboss.aesh.console.AeshConsoleImpl;
import org.jboss.aesh.console.Prompt;
import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
import org.jboss.aesh.console.command.registry.CommandRegistry;
import org.jboss.aesh.console.settings.Settings;
import org.jboss.aesh.console.settings.SettingsBuilder;
import org.keycloak.client.admin.cli.aesh.AeshEnhancer;
import org.keycloak.client.admin.cli.aesh.Globals;
import org.keycloak.client.admin.cli.aesh.ValveInputStream;
import org.keycloak.client.admin.cli.commands.KcAdmCmd;
import java.util.ArrayList;
import java.util.Arrays;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class KcAdmMain {
public static void main(String [] args) {
Globals.stdin = new ValveInputStream();
Settings settings = new SettingsBuilder()
.logging(false)
.readInputrc(false)
.disableCompletion(true)
.disableHistory(true)
.enableAlias(false)
.enableExport(false)
.inputStream(Globals.stdin)
.create();
CommandRegistry registry = new AeshCommandRegistryBuilder()
.command(KcAdmCmd.class)
.create();
AeshConsoleImpl console = (AeshConsoleImpl) new AeshConsoleBuilder()
.settings(settings)
.commandRegistry(registry)
.prompt(new Prompt(""))
// .commandInvocationProvider(new CommandInvocationServices() {
//
// })
.create();
AeshEnhancer.enhance(console);
// work around parser issues with quotes and brackets
ArrayList<String> arguments = new ArrayList<>();
arguments.add("kcadm");
arguments.addAll(Arrays.asList(args));
Globals.args = arguments;
StringBuilder b = new StringBuilder();
for (String s : args) {
// quote if necessary
boolean needQuote = false;
needQuote = s.indexOf(' ') != -1 || s.indexOf('\"') != -1 || s.indexOf('\'') != -1;
b.append(' ');
if (needQuote) {
b.append('\'');
}
b.append(s);
if (needQuote) {
b.append('\'');
}
}
console.setEcho(false);
console.execute("kcadm" + b.toString());
console.start();
}
}

View file

@ -0,0 +1,117 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.aesh;
import org.jboss.aesh.cl.parser.OptionParserException;
import org.jboss.aesh.cl.result.ResultHandler;
import org.jboss.aesh.console.AeshConsoleCallback;
import org.jboss.aesh.console.AeshConsoleImpl;
import org.jboss.aesh.console.ConsoleOperation;
import org.jboss.aesh.console.command.CommandNotFoundException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.container.CommandContainer;
import org.jboss.aesh.console.command.container.CommandContainerResult;
import org.jboss.aesh.console.command.invocation.AeshCommandInvocation;
import org.jboss.aesh.console.command.invocation.AeshCommandInvocationProvider;
import org.jboss.aesh.parser.AeshLine;
import org.jboss.aesh.parser.ParserStatus;
import java.lang.reflect.Method;
class AeshConsoleCallbackImpl extends AeshConsoleCallback {
private final AeshConsoleImpl console;
private CommandResult result;
AeshConsoleCallbackImpl(AeshConsoleImpl aeshConsole) {
this.console = aeshConsole;
}
@Override
@SuppressWarnings("unchecked")
public int execute(ConsoleOperation output) throws InterruptedException {
if (output != null && output.getBuffer().trim().length() > 0) {
ResultHandler resultHandler = null;
//AeshLine aeshLine = Parser.findAllWords(output.getBuffer());
AeshLine aeshLine = new AeshLine(output.getBuffer(), Globals.args, ParserStatus.OK, "");
try (CommandContainer commandContainer = getCommand(output, aeshLine)) {
resultHandler = commandContainer.getParser().getProcessedCommand().getResultHandler();
CommandContainerResult ccResult =
commandContainer.executeCommand(aeshLine, console.getInvocationProviders(), console.getAeshContext(),
new AeshCommandInvocationProvider().enhanceCommandInvocation(
new AeshCommandInvocation(console,
output.getControlOperator(), output.getPid(), this)));
result = ccResult.getCommandResult();
if(result == CommandResult.SUCCESS && resultHandler != null)
resultHandler.onSuccess();
else if(resultHandler != null)
resultHandler.onFailure(result);
if (result == CommandResult.FAILURE) {
// we assume the command has already output any error messages
System.exit(1);
}
} catch (Exception e) {
console.stop();
if (e instanceof OptionParserException) {
System.err.println("Unknown command: " + aeshLine.getWords().get(0));
} else {
System.err.println(e.getMessage());
}
if (Globals.dumpTrace) {
e.printStackTrace();
}
System.exit(1);
}
}
// empty line
else if (output != null) {
result = CommandResult.FAILURE;
}
else {
//stop();
result = CommandResult.FAILURE;
}
if (result == CommandResult.SUCCESS) {
return 0;
} else {
return 1;
}
}
private CommandContainer getCommand(ConsoleOperation output, AeshLine aeshLine) throws CommandNotFoundException {
Method m;
try {
m = console.getClass().getDeclaredMethod("getCommand", AeshLine.class, String.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unexpected error: ", e);
}
m.setAccessible(true);
try {
return (CommandContainer) m.invoke(console, aeshLine, output.getBuffer());
} catch (Exception e) {
throw new RuntimeException("Unexpected error: ", e);
}
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.aesh;
import org.jboss.aesh.console.AeshConsoleImpl;
import org.jboss.aesh.console.Console;
import java.lang.reflect.Field;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class AeshEnhancer {
public static void enhance(AeshConsoleImpl console) {
try {
Globals.stdin.setConsole(console);
Field field = AeshConsoleImpl.class.getDeclaredField("console");
field.setAccessible(true);
Console internalConsole = (Console) field.get(console);
internalConsole.setConsoleCallback(new AeshConsoleCallbackImpl(console));
} catch (Exception e) {
throw new RuntimeException("Failed to install Aesh enhancement", e);
}
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.aesh;
import java.util.List;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class Globals {
public static boolean dumpTrace = false;
public static ValveInputStream stdin;
public static List<String> args;
}

View file

@ -0,0 +1,89 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.aesh;
import org.jboss.aesh.console.AeshConsoleImpl;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* This stream blocks and waits, until there is a stream in the queue.
* It reads the stream to the end, then stops Aesh console.
*
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class ValveInputStream extends InputStream {
private BlockingQueue<InputStream> queue = new LinkedBlockingQueue<>(10);
private InputStream current;
private AeshConsoleImpl console;
@Override
public int read() throws IOException {
if (current == null) {
try {
current = queue.take();
} catch (InterruptedException e) {
throw new InterruptedIOException("Signalled to exit");
}
}
int c = current.read();
if (c == -1) {
//current = null;
if (console != null) {
console.stop();
}
}
return c;
}
/**
* For some reason AeshInputStream wants to do blocking read of whole buffers, which for stdin
* results in blocked input.
*/
@Override
public int read(byte b[], int off, int len) throws IOException {
int c = read();
if (c == -1) {
return c;
}
b[off] = (byte) c;
return 1;
}
public void setInputStream(InputStream is) {
if (queue.contains(is)) {
return;
}
queue.add(is);
}
public void setConsole(AeshConsoleImpl console) {
this.console = console;
}
public boolean isStdinAvailable() {
return console.isRunning();
}
}

View file

@ -0,0 +1,267 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.ConfigHandler;
import org.keycloak.client.admin.cli.config.FileConfigHandler;
import org.keycloak.client.admin.cli.config.InMemoryConfigHandler;
import org.keycloak.client.admin.cli.config.RealmConfigData;
import org.keycloak.client.admin.cli.util.ConfigUtil;
import org.keycloak.client.admin.cli.util.HttpUtil;
import org.keycloak.client.admin.cli.util.IoUtil;
import java.io.File;
import static org.keycloak.client.admin.cli.config.FileConfigHandler.setConfigFile;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CLIENT;
import static org.keycloak.client.admin.cli.util.ConfigUtil.checkAuthInfo;
import static org.keycloak.client.admin.cli.util.ConfigUtil.checkServerInfo;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractAuthOptionsCmd extends AbstractGlobalOptionsCmd {
@Option(shortName = 'a', name = "admin-root", description = "URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin")
String adminRestRoot;
@Option(name = "config", description = "Path to the config file (~/.keycloak/kcadm.config by default)")
String config;
@Option(name = "no-config", description = "No configuration file should be used, no authentication info should be saved", hasValue = false)
boolean noconfig;
@Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080/auth')")
String server;
@Option(shortName = 'r', name = "target-realm", description = "Realm to target - when it's different than the realm we authenticate against")
String targetRealm;
@Option(name = "realm", description = "Realm name to authenticate against")
String realm;
@Option(name = "client", description = "Realm name to authenticate against")
String clientId;
@Option(name = "user", description = "Username to login with")
String user;
@Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)")
String password;
@Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)")
String secret;
@Option(name = "keystore", description = "Path to a keystore containing private key")
String keystore;
@Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)")
String storePass;
@Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n otherwise defaults to keystore password)")
String keyPass;
@Option(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)")
String alias;
@Option(name = "truststore", description = "Path to a truststore")
String trustStore;
@Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)")
String trustPass;
protected void initFromParent(AbstractAuthOptionsCmd parent) {
super.initFromParent(parent);
noconfig = parent.noconfig;
config = parent.config;
server = parent.server;
realm = parent.realm;
clientId = parent.clientId;
user = parent.user;
password = parent.password;
secret = parent.secret;
keystore = parent.keystore;
storePass = parent.storePass;
keyPass = parent.keyPass;
alias = parent.alias;
trustStore = parent.trustStore;
trustPass = parent.trustPass;
}
protected void applyDefaultOptionValues() {
if (clientId == null) {
clientId = DEFAULT_CLIENT;
}
}
protected boolean noOptions() {
return server == null && realm == null && clientId == null && secret == null &&
user == null && password == null &&
keystore == null && storePass == null && keyPass == null && alias == null &&
trustStore == null && trustPass == null && config == null && (args == null || args.size() == 0);
}
protected String getTargetRealm(ConfigData config) {
return targetRealm != null ? targetRealm : config.getRealm();
}
protected void processGlobalOptions() {
super.processGlobalOptions();
if (config != null && noconfig) {
throw new RuntimeException("Options --config and --no-config are mutually exclusive");
}
if (!noconfig) {
setConfigFile(config != null ? config : ConfigUtil.DEFAULT_CONFIG_FILE_PATH);
ConfigUtil.setHandler(new FileConfigHandler());
} else {
InMemoryConfigHandler handler = new InMemoryConfigHandler();
ConfigData data = new ConfigData();
initConfigData(data);
handler.setConfigData(data);
ConfigUtil.setHandler(handler);
}
}
protected void setupTruststore(ConfigData configData, CommandInvocation invocation ) {
if (!configData.getServerUrl().startsWith("https:")) {
return;
}
String truststore = trustStore;
if (truststore == null) {
truststore = configData.getTruststore();
}
if (truststore != null) {
String pass = trustPass;
if (pass == null) {
pass = configData.getTrustpass();
}
if (pass == null) {
pass = IoUtil.readSecret("Enter truststore password: ", invocation);
}
try {
HttpUtil.setTruststore(new File(truststore), pass);
} catch (Exception e) {
throw new RuntimeException("Failed to load truststore: " + truststore, e);
}
}
}
protected ConfigData ensureAuthInfo(ConfigData config, CommandInvocation commandInvocation) {
if (requiresLogin()) {
// make sure current handler is in-memory handler
// restore it at the end
ConfigHandler old = ConfigUtil.getHandler();
try {
// make sure all defaults are initialized after this point
applyDefaultOptionValues();
initConfigData(config);
ConfigUtil.setupInMemoryHandler(config);
ConfigCredentialsCmd login = new ConfigCredentialsCmd();
login.initFromParent(this);
login.init(config);
login.process(commandInvocation);
// this must be executed before finally block which restores config handler
return loadConfig();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
ConfigUtil.setHandler(old);
}
} else {
checkAuthInfo(config);
// make sure all defaults are initialized after this point
applyDefaultOptionValues();
return loadConfig();
}
}
protected boolean requiresLogin() {
return user != null || password != null || secret != null || keystore != null
|| keyPass != null || storePass != null || alias != null;
}
protected ConfigData copyWithServerInfo(ConfigData config) {
ConfigData result = config.deepcopy();
if (server != null) {
result.setServerUrl(server);
}
if (realm != null) {
result.setRealm(realm);
}
checkServerInfo(result);
return result;
}
private void initConfigData(ConfigData data) {
if (server != null)
data.setServerUrl(server);
if (realm != null)
data.setRealm(realm);
if (trustStore != null)
data.setTruststore(trustStore);
RealmConfigData rdata = data.sessionRealmConfigData();
if (clientId != null)
rdata.setClientId(clientId);
if (secret != null)
rdata.setSecret(secret);
}
protected void checkUnsupportedOptions(String ... options) {
if (options.length % 2 != 0) {
throw new IllegalArgumentException("Even number of argument required");
}
for (int i = 0; i < options.length; i++) {
String name = options[i];
String value = options[++i];
if (value != null) {
throw new RuntimeException("Unsupported option: " + name);
}
}
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.commands;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.Command;
import org.keycloak.client.admin.cli.aesh.Globals;
import org.keycloak.client.admin.cli.util.FilterUtil;
import org.keycloak.client.admin.cli.util.ReturnFields;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import static org.keycloak.client.admin.cli.util.HttpUtil.normalize;
import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractGlobalOptionsCmd implements Command {
@Option(shortName = 'x', description = "Print full stack trace when exiting with error", hasValue = false)
boolean dumpTrace;
@Option(name = "help", description = "Print command specific help", hasValue = false)
boolean help;
// we don't want Aesh to handle illegal options
@Arguments
List<String> args;
protected void initFromParent(AbstractGlobalOptionsCmd parent) {
dumpTrace = parent.dumpTrace;
help = parent.help;
args = parent.args;
}
protected void processGlobalOptions() {
Globals.dumpTrace = dumpTrace;
}
protected boolean printHelp() {
if (help || nothingToDo()) {
printOut(help());
return true;
}
return false;
}
protected boolean nothingToDo() {
return false;
}
protected String help() {
return KcAdmCmd.usage();
}
protected String composeAdminRoot(String server) {
return normalize(server) + "admin";
}
protected void requireValue(Iterator<String> it, String option) {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
}
protected String extractTypeNameFromUri(String resourceUrl) {
String type = extractLastComponentOfUri(resourceUrl);
if (type.endsWith("s")) {
type = type.substring(0, type.length()-1);
}
return type;
}
protected String extractLastComponentOfUri(String resourceUrl) {
int endPos = resourceUrl.endsWith("/") ? resourceUrl.length()-2 : resourceUrl.length()-1;
int pos = resourceUrl.lastIndexOf("/", endPos);
pos = pos == -1 ? 0 : pos;
return resourceUrl.substring(pos+1, endPos+1);
}
protected JsonNode applyFieldFilter(ObjectMapper mapper, JsonNode rootNode, ReturnFields returnFields) {
// construct new JsonNode that satisfies filtering specified by returnFields
try {
return FilterUtil.copyFilteredObject(rootNode, returnFields);
} catch (IOException e) {
throw new RuntimeException("Failed to apply fields filter", e);
}
}
}

View file

@ -0,0 +1,435 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.commands;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.admin.cli.common.AttributeOperation;
import org.keycloak.client.admin.cli.common.CmdStdinContext;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.util.AccessibleBufferOutputStream;
import org.keycloak.client.admin.cli.util.Header;
import org.keycloak.client.admin.cli.util.Headers;
import org.keycloak.client.admin.cli.util.HeadersBody;
import org.keycloak.client.admin.cli.util.HeadersBodyStatus;
import org.keycloak.client.admin.cli.util.HttpUtil;
import org.keycloak.client.admin.cli.util.OutputFormat;
import org.keycloak.client.admin.cli.util.ReflectionUtil;
import org.keycloak.client.admin.cli.util.ReturnFields;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.DELETE;
import static org.keycloak.client.admin.cli.common.AttributeOperation.Type.SET;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.HttpUtil.checkSuccess;
import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl;
import static org.keycloak.client.admin.cli.util.HttpUtil.doGet;
import static org.keycloak.client.admin.cli.util.IoUtil.copyStream;
import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
import static org.keycloak.client.admin.cli.util.IoUtil.printOut;
import static org.keycloak.client.admin.cli.util.OutputUtil.MAPPER;
import static org.keycloak.client.admin.cli.util.OutputUtil.printAsCsv;
import static org.keycloak.client.admin.cli.util.ParseUtil.mergeAttributes;
import static org.keycloak.client.admin.cli.util.ParseUtil.parseFileOrStdin;
import static org.keycloak.client.admin.cli.util.ParseUtil.parseKeyVal;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd {
String file;
String fields;
boolean printHeaders;
boolean returnId;
boolean outputResult;
boolean compressed;
boolean unquoted;
boolean mergeMode;
boolean noMerge;
Integer offset;
Integer limit;
String format = "json";
OutputFormat outputFormat;
String httpVerb;
Headers headers = new Headers();
List<AttributeOperation> attrs = new LinkedList<>();
Map<String, String> filter = new HashMap<>();
String url = null;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
initOptions();
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
processOptions(commandInvocation);
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
abstract void initOptions();
abstract String suggestHelp();
void processOptions(CommandInvocation commandInvocation) {
if (args == null || args.isEmpty()) {
throw new IllegalArgumentException("URI not specified");
}
Iterator<String> it = args.iterator();
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "-s":
case "--set": {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
String[] keyVal = parseKeyVal(it.next());
attrs.add(new AttributeOperation(SET, keyVal[0], keyVal[1]));
break;
}
case "-d":
case "--delete": {
attrs.add(new AttributeOperation(DELETE, it.next()));
break;
}
case "-h":
case "--header": {
requireValue(it, option);
String[] keyVal = parseKeyVal(it.next());
headers.add(keyVal[0], keyVal[1]);
break;
}
case "-q":
case "--query": {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
String arg = it.next();
String[] keyVal;
if (arg.indexOf("=") == -1) {
keyVal = new String[] {"", arg};
} else {
keyVal = parseKeyVal(arg);
}
filter.put(keyVal[0], keyVal[1]);
break;
}
default: {
if (url == null) {
url = option;
} else {
throw new IllegalArgumentException("Invalid option: " + option);
}
}
}
}
if (url == null) {
throw new IllegalArgumentException("Resource URI not specified");
}
if (outputResult && returnId) {
throw new IllegalArgumentException("Options -o and -i are mutually exclusive");
}
try {
outputFormat = OutputFormat.valueOf(format.toUpperCase());
} catch (Exception e) {
throw new RuntimeException("Unsupported output format: " + format);
}
if (mergeMode && noMerge) {
throw new IllegalArgumentException("Options --merge and --no-merge are mutually exclusive");
}
if (file == null && attrs.size() > 0 && !noMerge) {
mergeMode = true;
}
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
// see if Content-Type header is explicitly set to non-json value
Header ctype = headers.get("content-type");
InputStream body = null;
CmdStdinContext<JsonNode> ctx = new CmdStdinContext<>();
if (file != null) {
if (ctype != null && !"application/json".equals(ctype.getValue())) {
if ("-".equals(file)) {
body = System.in;
} else {
try {
body = new BufferedInputStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
throw new RuntimeException("File not found: " + file);
}
}
} else {
ctx = parseFileOrStdin(file);
}
}
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config, commandInvocation);
String auth = null;
config = ensureAuthInfo(config, commandInvocation);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
}
auth = auth != null ? "Bearer " + auth : null;
if (auth != null) {
headers.addIfMissing("Authorization", auth);
}
final String server = config.getServerUrl();
final String realm = getTargetRealm(config);
final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
String resourceUrl = composeResourceUrl(adminRoot, realm, url);
String typeName = extractTypeNameFromUri(resourceUrl);
if (filter.size() > 0) {
resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, filter);
}
headers.addIfMissing("Accept", "application/json");
if (isUpdate() && mergeMode) {
ObjectNode result;
HeadersBodyStatus response;
try {
response = HttpUtil.doGet(resourceUrl, new HeadersBody(headers));
checkSuccess(resourceUrl, response);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
copyStream(response.getBody(), buffer);
result = MAPPER.readValue(buffer.toByteArray(), ObjectNode.class);
} catch (IOException e) {
throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
}
CmdStdinContext<JsonNode> ctxremote = new CmdStdinContext<>();
ctxremote.setResult(result);
// merge local representation over remote one
if (ctx.getResult() != null) {
ReflectionUtil.merge(ctx.getResult(), (ObjectNode) ctxremote.getResult());
}
ctx = ctxremote;
}
if (attrs.size() > 0) {
if (body != null) {
throw new RuntimeException("Can't set attributes on content of type other than application/json");
}
ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs);
}
if (body == null && ctx.getContent() != null) {
body = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8")));
}
ReturnFields returnFields = null;
if (fields != null) {
returnFields = new ReturnFields(fields);
}
// make sure content type is set
if (body != null) {
headers.addIfMissing("Content-Type", "application/json");
}
LinkedHashMap<String, String> queryParams = new LinkedHashMap<>();
if (offset != null) {
queryParams.put("first", String.valueOf(offset));
}
if (limit != null) {
queryParams.put("max", String.valueOf(limit));
}
if (queryParams.size() > 0) {
resourceUrl = HttpUtil.addQueryParamsToUri(resourceUrl, queryParams);
}
HeadersBodyStatus response;
try {
response = HttpUtil.doRequest(httpVerb, resourceUrl, new HeadersBody(headers, body));
} catch (IOException e) {
throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
}
// output response
if (printHeaders) {
printOut(response.getStatus());
for (Header header : response.getHeaders()) {
printOut(header.getName() + ": " + header.getValue());
}
}
checkSuccess(resourceUrl, response);
AccessibleBufferOutputStream abos = new AccessibleBufferOutputStream(System.out);
if (response.getBody() == null) {
throw new RuntimeException("Internal error - response body should never be null");
}
if (printHeaders) {
printOut("");
}
Header location = response.getHeaders().get("Location");
String id = location != null ? extractLastComponentOfUri(location.getValue()) : null;
if (id != null) {
if (returnId) {
printOut(id);
} else if (!outputResult) {
printErr("Created new " + typeName + " with id '" + id + "'");
}
}
if (outputResult) {
if (isCreateOrUpdate() && (response.getStatusCode() == 204 || id != null)) {
// get object for id
headers = new Headers();
if (auth != null) {
headers.add("Authorization", auth);
}
try {
String fetchUrl = id != null ? (resourceUrl + "/" + id) : resourceUrl;
response = doGet(fetchUrl, new HeadersBody(headers));
} catch (IOException e) {
throw new RuntimeException("HTTP request error: " + e.getMessage(), e);
}
}
Header contentType = response.getHeaders().get("content-type");
boolean canPrettyPrint = contentType != null && contentType.getValue().equals("application/json");
boolean pretty = !compressed;
if (canPrettyPrint && (pretty || returnFields != null)) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
copyStream(response.getBody(), buffer);
try {
JsonNode rootNode = MAPPER.readValue(buffer.toByteArray(), JsonNode.class);
if (returnFields != null) {
rootNode = applyFieldFilter(MAPPER, rootNode, returnFields);
}
if (outputFormat == OutputFormat.JSON) {
// now pretty print it to output
MAPPER.writeValue(abos, rootNode);
} else {
printAsCsv(rootNode, returnFields, unquoted);
}
} catch (Exception ignored) {
copyStream(new ByteArrayInputStream(buffer.toByteArray()), abos);
}
} else {
copyStream(response.getBody(), abos);
}
}
int lastByte = abos.getLastByte();
if (lastByte != -1 && lastByte != 13 && lastByte != 10) {
printErr("");
}
return CommandResult.SUCCESS;
}
private boolean isUpdate() {
return "put".equals(httpVerb);
}
private boolean isCreateOrUpdate() {
return "post".equals(httpVerb) || "put".equals(httpVerb);
}
}

View file

@ -0,0 +1,334 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.commands;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.operations.ClientOperations;
import org.keycloak.client.admin.cli.operations.GroupOperations;
import org.keycloak.client.admin.cli.operations.RoleOperations;
import org.keycloak.client.admin.cli.operations.LocalSearch;
import org.keycloak.client.admin.cli.operations.UserOperations;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import static org.keycloak.client.admin.cli.util.AuthUtil.ensureToken;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.ConfigUtil.credentialsAvailable;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "add-roles", description = "[ARGUMENTS]")
public class AddRolesCmd extends AbstractAuthOptionsCmd {
@Option(name = "uusername", description = "Target user's 'username'")
String uusername;
@Option(name = "uid", description = "Target user's 'id'")
String uid;
@Option(name = "gname", description = "Target group's 'name'")
String gname;
@Option(name = "gpath", description = "Target group's 'path'")
String gpath;
@Option(name = "gid", description = "Target group's 'id'")
String gid;
@Option(name = "cclientid", description = "Target client's 'clientId'")
String cclientid;
@Option(name = "cid", description = "Target client's 'id'")
String cid;
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<String> roleNames = new LinkedList<>();
List<String> roleIds = new LinkedList<>();
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
Iterator<String> it = args.iterator();
while (it.hasNext()) {
String option = it.next();
switch (option) {
case "--rolename": {
optionRequiresValueCheck(it, option);
roleNames.add(it.next());
break;
}
case "--roleid": {
optionRequiresValueCheck(it, option);
roleIds.add(it.next());
break;
}
default: {
throw new IllegalArgumentException("Invalid option: " + option);
}
}
}
if (uid != null && uusername != null) {
throw new IllegalArgumentException("Incompatible options: --uid and --uusername are mutually exclusive");
}
if ((gid != null && gname != null) || (gid != null && gpath != null) || (gname != null && gpath != null)) {
throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive");
}
if (roleNames.isEmpty() && roleIds.isEmpty()) {
throw new IllegalArgumentException("No role specified. Use --rolename or --roleid to specify roles");
}
if (cid != null && cclientid != null) {
throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive");
}
if (isUserSpecified() && isGroupSpecified()) {
throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath");
}
if (!isUserSpecified() && !isGroupSpecified()) {
throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
}
ConfigData config = loadConfig();
config = copyWithServerInfo(config);
setupTruststore(config, commandInvocation);
String auth = null;
config = ensureAuthInfo(config, commandInvocation);
config = copyWithServerInfo(config);
if (credentialsAvailable(config)) {
auth = ensureToken(config);
}
auth = auth != null ? "Bearer " + auth : null;
final String server = config.getServerUrl();
final String realm = getTargetRealm(config);
final String adminRoot = adminRestRoot != null ? adminRestRoot : composeAdminRoot(server);
if (isUserSpecified()) {
if (uid == null) {
uid = UserOperations.getIdFromUsername(adminRoot, realm, auth, uusername);
}
if (isClientSpecified()) {
// list client roles for a user
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now add all the roles
UserOperations.addClientRoles(adminRoot, realm, auth, uid, cid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now add all the roles
UserOperations.addRealmRoles(adminRoot, realm, auth, uid, new ArrayList<>(rolesToAdd));
}
} else if (isGroupSpecified()) {
if (gname != null) {
gid = GroupOperations.getIdFromName(adminRoot, realm, auth, gname);
} else if (gpath != null) {
gid = GroupOperations.getIdFromPath(adminRoot, realm, auth, gpath);
}
if (isClientSpecified()) {
// list client roles for a group
if (cid == null) {
cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid);
}
List<ObjectNode> roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth);
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles));
// now add all the roles
GroupOperations.addClientRoles(adminRoot, realm, auth, gid, cid, new ArrayList<>(rolesToAdd));
} else {
Set<ObjectNode> rolesToAdd = getRoleRepresentations(roleNames, roleIds,
new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth)));
// now add all the roles
GroupOperations.addRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd));
}
} else {
throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group");
}
return CommandResult.SUCCESS;
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
private Set<ObjectNode> getRoleRepresentations(List<String> roleNames, List<String> roleIds, LocalSearch roleSearch) {
Set<ObjectNode> rolesToAdd = new HashSet<>();
// now we process roles
for (String name : roleNames) {
ObjectNode r = roleSearch.exactMatchOne(name, "name");
if (r == null) {
throw new RuntimeException("Role not found for name: " + name);
}
rolesToAdd.add(r);
}
for (String id : roleIds) {
ObjectNode r = roleSearch.exactMatchOne(id, "id");
if (r == null) {
throw new RuntimeException("Role not found for id: " + id);
}
rolesToAdd.add(r);
}
return rolesToAdd;
}
private void optionRequiresValueCheck(Iterator<String> it, String option) {
if (!it.hasNext()) {
throw new IllegalArgumentException("Option " + option + " requires a value");
}
}
private boolean isClientSpecified() {
return cid != null || cclientid != null;
}
private boolean isGroupSpecified() {
return gid != null || gname != null || gpath != null;
}
private boolean isUserSpecified() {
return uid != null || uusername != null;
}
@Override
protected boolean nothingToDo() {
return noOptions() && uusername == null && uid == null && cclientid == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help add-roles' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
out.println("Usage: " + CMD + " add-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]");
out.println();
out.println("Command to add realm or client roles to a user or group.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("If client is specified using --cclientid or --cid then roles to add are client roles, otherwise they are realm roles.");
out.println("Either a user, or a group needs to be specified. If user is specified using --uusername or --uid then roles are added");
out.println("to a specific user. If group is specified using --gname, --gpath or --gid then roles are added to a specific group.");
out.println("One or more roles have to be specified using --rolename or --roleid so that they are added to a group or a user.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" --uusername User's 'username'. If more than one user exists with the same username");
out.println(" you'll have to use --uid to specify the target user");
out.println(" --uid User's 'id' attribute");
out.println(" --gname Group's 'name'. If more than one group exists with the same name you'll have");
out.println(" to use --gid, or --gpath to specify the target group");
out.println(" --gpath Group's 'path' attribute");
out.println(" --gid Group's 'id' attribute");
out.println(" --cclientid Client's 'clientId' attribute");
out.println(" --cid Client's 'id' attribute");
out.println(" --rolename Role's 'name' attribute");
out.println(" --roleid Role's 'id' attribute");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println();
out.println("Examples:");
out.println();
out.println("Add 'offline_access' realm role to a user:");
out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --uusername testuser --rolename offline_access");
out.println();
out.println("Add 'realm-management' client roles 'view-users', 'view-clients' and 'view-realm' to a user:");
out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --uusername testuser --cclientid realm-management --rolename view-users --rolename view-clients --rolename view-realm");
out.println();
out.println("Add 'uma_authorization' realm role to a group:");
out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --gname PowerUsers --rolename uma_authorization");
out.println();
out.println("Add 'realm-management' client roles 'realm-admin' to a group:");
out.println(" " + PROMPT + " " + CMD + " add-roles -r demorealm --gname PowerUsers --cclientid realm-management --rolename realm-admin");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.GroupCommandDefinition;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import java.io.PrintWriter;
import java.io.StringWriter;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@GroupCommandDefinition(name = "config", description = "COMMAND [ARGUMENTS]", groupCommands = {ConfigCredentialsCmd.class} )
public class ConfigCmd extends AbstractAuthOptionsCmd {
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (args != null && args.size() > 0) {
String cmd = args.get(0);
switch (cmd) {
case "credentials": {
args.remove(0);
ConfigCredentialsCmd command = new ConfigCredentialsCmd();
command.initFromParent(this);
return command.execute(commandInvocation);
}
case "truststore": {
args.remove(0);
ConfigTruststoreCmd command = new ConfigTruststoreCmd();
command.initFromParent(this);
return command.execute(commandInvocation);
}
default: {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
throw new IllegalArgumentException("Unknown sub-command: " + cmd + suggestHelp());
}
}
}
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
throw new IllegalArgumentException("Sub-command required by '" + CMD + " config' - one of: 'credentials', 'truststore'");
} finally {
commandInvocation.stop();
}
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " config SUB_COMMAND [ARGUMENTS]");
out.println();
out.println("Where SUB_COMMAND is one of: 'credentials', 'truststore'");
out.println();
out.println();
out.println("Use '" + CMD + " help config SUB_COMMAND' for more info.");
out.println("Use '" + CMD + " help' for general information and a list of commands.");
return sb.toString();
}
}

View file

@ -0,0 +1,275 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.admin.cli.config.ConfigData;
import org.keycloak.client.admin.cli.config.RealmConfigData;
import org.keycloak.client.admin.cli.util.AuthUtil;
import org.keycloak.representations.AccessTokenResponse;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokens;
import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokensByJWT;
import static org.keycloak.client.admin.cli.util.AuthUtil.getAuthTokensBySecret;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.ConfigUtil.getHandler;
import static org.keycloak.client.admin.cli.util.ConfigUtil.loadConfig;
import static org.keycloak.client.admin.cli.util.ConfigUtil.saveTokens;
import static org.keycloak.client.admin.cli.util.IoUtil.printErr;
import static org.keycloak.client.admin.cli.util.IoUtil.readSecret;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "credentials", description = "--server SERVER_URL --realm REALM [ARGUMENTS]")
public class ConfigCredentialsCmd extends AbstractAuthOptionsCmd {
private int sigLifetime = 600;
public void init(ConfigData configData) {
if (server == null) {
server = configData.getServerUrl();
}
if (realm == null) {
realm = configData.getRealm();
}
if (trustStore == null) {
trustStore = configData.getTruststore();
}
RealmConfigData rdata = configData.getRealmConfigData(server, realm);
if (rdata == null) {
return;
}
if (clientId == null) {
clientId = rdata.getClientId();
}
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
processGlobalOptions();
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
@Override
protected boolean nothingToDo() {
return noOptions();
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
// check server
if (server == null) {
throw new IllegalArgumentException("Required option not specified: --server");
}
try {
new URL(server);
} catch (Exception e) {
throw new RuntimeException("Invalid server endpoint url: " + server, e);
}
if (realm == null)
throw new IllegalArgumentException("Required option not specified: --realm");
String signedRequestToken = null;
boolean clientSet = clientId != null;
applyDefaultOptionValues();
if (user != null) {
printErr("Logging into " + server + " as user " + user + " of realm " + realm);
// if user was set there needs to be a password so we can authenticate
if (password == null) {
password = readSecret("Enter password: ", commandInvocation);
}
// if secret was set to be read from stdin, then ask for it
if ("-".equals(secret) && keystore == null) {
secret = readSecret("Enter client secret: ", commandInvocation);
}
} else if (keystore != null || secret != null || clientSet) {
printErr("Logging into " + server + " as " + "service-account-" + clientId + " of realm " + realm);
if (keystore == null) {
if (secret == null) {
secret = readSecret("Enter client secret: ", commandInvocation);
}
}
}
if (keystore != null) {
if (secret != null) {
throw new IllegalArgumentException("Can't use both --keystore and --secret");
}
if (!new File(keystore).isFile()) {
throw new RuntimeException("No such keystore file: " + keystore);
}
if (storePass == null) {
storePass = readSecret("Enter keystore password: ", commandInvocation);
keyPass = readSecret("Enter key password: ", commandInvocation);
}
if (keyPass == null) {
keyPass = storePass;
}
if (alias == null) {
alias = clientId;
}
String realmInfoUrl = server + "/realms/" + realm;
signedRequestToken = AuthUtil.getSignedRequestToken(keystore, storePass, keyPass,
alias, sigLifetime, clientId, realmInfoUrl);
}
// if only server and realm are set, just save config and be done
if (user == null && secret == null && keystore == null) {
getHandler().saveMergeConfig(config -> {
config.setServerUrl(server);
config.setRealm(realm);
});
return CommandResult.SUCCESS;
}
setupTruststore(copyWithServerInfo(loadConfig()), commandInvocation);
// now use the token endpoint to retrieve access token, and refresh token
AccessTokenResponse tokens = signedRequestToken != null ?
getAuthTokensByJWT(server, realm, user, password, clientId, signedRequestToken) :
secret != null ?
getAuthTokensBySecret(server, realm, user, password, clientId, secret) :
getAuthTokens(server, realm, user, password, clientId);
Long sigExpiresAt = signedRequestToken == null ? null : System.currentTimeMillis() + sigLifetime * 1000;
// save tokens to config file
saveTokens(tokens, server, realm, clientId, signedRequestToken, sigExpiresAt, secret);
return CommandResult.SUCCESS;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config credentials' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " config credentials --server SERVER_URL --realm REALM --user USER [--password PASSWORD] [ARGUMENTS]");
out.println(" " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--secret SECRET] [ARGUMENTS]");
out.println(" " + CMD + " config credentials --server SERVER_URL --realm REALM --client CLIENT_ID [--keystore KEYSTORE] [ARGUMENTS]");
out.println();
out.println("Command to establish an authenticated client session with the server. There are many authentication");
out.println("options available, and it depends on server side client authentication configuration how client can or should authenticate.");
out.println("The information always required includes --server, and --realm. Then, --user and / or --client need to be used to authenticate.");
out.println("If --client is not provided it defaults to 'admin-cli'. The authentication options / requirements depend on how this client is configured.");
out.println();
out.println("If confidential client authentication is also configured, you may have to specify a client id, and client credentials in addition to");
out.println("user credentials. Client credentials are either a client secret, or a keystore information to use Signed JWT mechanism.");
out.println("If only client credentials are provided, and no user credentials, then the service account is used for login.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to a config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println();
out.println(" Command specific options:");
out.println(" --server SERVER_URL Server endpoint url (e.g. 'http://localhost:8080/auth')");
out.println(" --realm REALM Realm name to use");
out.println(" --user USER Username to login with");
out.println(" --password PASSWORD Password to login with (prompted for if not specified and --user is used)");
out.println(" --client CLIENT_ID ClientId used by this client tool ('admin-cli' by default)");
out.println(" --secret SECRET Secret to authenticate the client (prompted for if --client is specified, and no --keystore is specified)");
out.println(" --keystore PATH Path to a keystore containing private key");
out.println(" --storepass PASSWORD Keystore password (prompted for if not specified and --keystore is used)");
out.println(" --keypass PASSWORD Key password (prompted for if not specified and --keystore is used without --storepass,");
out.println(" otherwise defaults to keystore password)");
out.println(" --alias ALIAS Alias of the key inside a keystore (defaults to the value of ClientId)");
out.println();
out.println();
out.println("Examples:");
out.println();
out.println("Login as 'admin' user of 'master' realm to a local Keycloak server running on default port.");
out.println("You will be prompted for a password:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:8080/auth --realm master --user admin");
out.println();
out.println("Login to Keycloak server at non-default endpoint passing the password via standard input:");
if (OS_ARCH.isWindows()) {
out.println(" " + PROMPT + " echo mypassword | " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin");
} else {
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin << EOF");
out.println(" mypassword");
out.println(" EOF");
}
out.println();
out.println("Login specifying a password through command line:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user admin --password " + OS_ARCH.envVar("PASSWORD"));
out.println();
out.println("Login using a client service account of a custom client. You will be prompted for a client secret:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli");
out.println();
out.println("Login using a client service account of a custom client, authenticating with signed JWT.");
out.println("You will be prompted for a keystore password, and a key password:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
out.println();
out.println("Login as 'user' while also authenticating a custom client with signed JWT.");
out.println("You will be prompted for a user password, a keystore password, and a key password:");
out.println(" " + PROMPT + " " + CMD + " config credentials --server http://localhost:9080/auth --realm master --user user --client reg-cli --keystore " + OS_ARCH.path("~/.keycloak/keystore.jks"));
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,200 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.CommandException;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.ConfigUtil.saveMergeConfig;
import static org.keycloak.client.admin.cli.util.IoUtil.readSecret;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "truststore", description = "PATH [ARGUMENTS]")
public class ConfigTruststoreCmd extends AbstractAuthOptionsCmd {
private ConfigCmd parent;
private boolean delete;
protected void initFromParent(ConfigCmd parent) {
this.parent = parent;
super.initFromParent(parent);
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
if (printHelp()) {
return help ? CommandResult.SUCCESS : CommandResult.FAILURE;
}
return process(commandInvocation);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + suggestHelp(), e);
} finally {
commandInvocation.stop();
}
}
@Override
protected boolean nothingToDo() {
return noOptions();
}
public CommandResult process(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
List<String> args = new ArrayList<>();
Iterator<String> it = parent.args.iterator();
while (it.hasNext()) {
String arg = it.next();
switch (arg) {
case "-d":
case "--delete": {
delete = true;
break;
}
default: {
args.add(arg);
}
}
}
if (args.size() > 1) {
throw new IllegalArgumentException("Invalid option: " + args.get(1));
}
String truststore = null;
if (args.size() > 0) {
truststore = args.get(0);
}
checkUnsupportedOptions("--server", server,
"--realm", realm,
"--client", clientId,
"--user", user,
"--password", password,
"--secret", secret,
"--truststore", trustStore,
"--keystore", keystore,
"--keypass", keyPass,
"--alias", alias);
// now update the config
processGlobalOptions();
String store;
String pass;
if (!delete) {
if (truststore == null) {
throw new IllegalArgumentException("No truststore specified");
}
if (!new File(truststore).isFile()) {
throw new RuntimeException("Truststore file not found: " + truststore);
}
if ("-".equals(trustPass)) {
trustPass = readSecret("Enter truststore password: ", commandInvocation);
}
store = truststore;
pass = trustPass;
} else {
if (truststore != null) {
throw new IllegalArgumentException("Option --delete is mutually exclusive with specifying a TRUSTSTORE");
}
if (trustPass != null) {
throw new IllegalArgumentException("Options --trustpass and --delete are mutually exclusive");
}
store = null;
pass = null;
}
saveMergeConfig(config -> {
config.setTruststore(store);
config.setTrustpass(pass);
});
return CommandResult.SUCCESS;
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help config truststore' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " config truststore [TRUSTSTORE | --delete] [--trustpass PASSWORD] [ARGUMENTS]");
out.println();
out.println("Command to configure a global truststore to use when using https to connect to Keycloak server.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println();
out.println(" Command specific options:");
out.println(" TRUSTSTORE Path to truststore file");
out.println(" --trustpass PASSWORD Truststore password to unlock truststore (prompted for if set to '-')");
out.println(" -d, --delete Remove truststore configuration");
out.println();
out.println();
out.println("Examples:");
out.println();
out.println("Specify a truststore - you will be prompted for truststore password every time it is used:");
out.println(" " + PROMPT + " " + CMD + " config truststore " + OS_ARCH.path("~/.keycloak/truststore.jks"));
out.println();
out.println("Specify a truststore, and password - truststore will automatically be used without prompting for password:");
out.println(" " + PROMPT + " " + CMD + " config truststore --storepass " + OS_ARCH.envVar("PASSWORD") + " " + OS_ARCH.path("~/.keycloak/truststore.jks"));
out.println();
out.println("Remove truststore configuration:");
out.println(" " + PROMPT + " " + CMD + " config truststore --delete");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

View file

@ -0,0 +1,167 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.client.admin.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import java.io.PrintWriter;
import java.io.StringWriter;
import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING;
import static org.keycloak.client.admin.cli.util.OsUtil.CMD;
import static org.keycloak.client.admin.cli.util.OsUtil.EOL;
import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH;
import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@CommandDefinition(name = "create", description = "Command to create new resources")
public class CreateCmd extends AbstractRequestCmd {
@Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true)
String file;
@Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header", hasValue = true)
String fields;
@Option(shortName = 'H', name = "print-headers", description = "Print response headers", hasValue = false)
boolean printHeaders;
@Option(shortName = 'i', name = "id", description = "After creation only print id of created resource to standard output", hasValue = false)
boolean returnId = false;
@Option(shortName = 'o', name = "output", description = "After creation output the new resource to standard output", hasValue = false)
boolean outputResult = false;
@Option(shortName = 'c', name = "compressed", description = "Don't pretty print the output", hasValue = false)
boolean compressed = false;
//@OptionGroup(shortName = 's', name = "set", description = "Set attribute to the specified value")
//Map<String, String> attributes = new LinkedHashMap<>();
@Override
void initOptions() {
// set options on parent
super.file = file;
super.fields = fields;
super.printHeaders = printHeaders;
super.returnId = returnId;
super.outputResult = outputResult;
super.compressed = compressed;
super.httpVerb = "post";
}
@Override
protected boolean nothingToDo() {
return noOptions() && file == null && (args == null || args.size() == 0);
}
protected String suggestHelp() {
return EOL + "Try '" + CMD + " help create' for more information";
}
protected String help() {
return usage();
}
public static String usage() {
StringWriter sb = new StringWriter();
PrintWriter out = new PrintWriter(sb);
out.println("Usage: " + CMD + " create ENDPOINT_URI [ARGUMENTS]");
out.println();
out.println("Command to create new resources on the server.");
out.println();
out.println("Use `" + CMD + " config credentials` to establish an authenticated sessions, or use CREDENTIALS OPTIONS");
out.println("to perform one time authentication.");
out.println();
out.println("Arguments:");
out.println();
out.println(" Global options:");
out.println(" -x Print full stack trace when exiting with error");
out.println(" --config Path to the config file (" + DEFAULT_CONFIG_FILE_STRING + " by default)");
out.println(" --truststore PATH Path to a truststore containing trusted certificates");
out.println(" --trustpass PASSWORD Truststore password (prompted for if not specified and --truststore is used)");
out.println(" CREDENTIALS OPTIONS Same set of options as accepted by '" + CMD + " config credentials' in order to establish");
out.println(" an authenticated sessions. This allows on-the-fly transient authentication that does");
out.println(" not touch a config file.");
out.println();
out.println(" Command specific options:");
out.println(" ENDPOINT_URI URI used to compose a target resource url. Commonly used values are:");
out.println(" realms, users, roles, groups, clients, keys, serverinfo, components ...");
out.println(" If it starts with 'http://' then it will be used as target resource url");
out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against");
out.println(" -s, --set NAME=VALUE Set a specific attribute NAME to a specified value VALUE");
out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body");
out.println(" -f, --file FILENAME Read object from file or standard input if FILENAME is set to '-'");
out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE");
out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE");
out.println();
out.println(" -H, --print-headers Print response headers");
out.println(" -o, --output After creation output the new resource to standard output");
out.println(" -i, --id After creation only print id of the new resource to standard output");
out.println(" -F, --fields FILTER A filter pattern to specify which fields of a JSON response to output");
out.println(" -c, --compressed Don't pretty print the output");
out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin");
out.println();
out.println();
out.println("Nested attributes are supported by using '.' to separate components of a KEY. Optionaly, the KEY components ");
out.println("can be quoted with double quotes - e.g. my_client.attributes.\"external.user.id\". If VALUE starts with [ and ");
out.println("ends with ] the attribute will be set as a JSON array. If VALUE starts with { and ends with } the attribute ");
out.println("will be set as a JSON object. If KEY ends with an array index - e.g. clients[3]=VALUE - then the specified item");
out.println("of the array is updated. If KEY+=VALUE syntax is used, then KEY is assumed to be an array, and another item is");
out.println("added to it.");
out.println();
out.println("Attributes can also be deleted. If KEY ends with an array index, then the targeted item of an array is removed");
out.println("and the following items are shifted.");
out.println();
out.println();
out.println("Examples:");
out.println();
out.println("Create a new realm:");
out.println(" " + PROMPT + " " + CMD + " create realms -s realm=demorealm -s enabled=true");
out.println();
out.println("Create a new realm role in realm 'demorealm' returning newly created role:");
out.println(" " + PROMPT + " " + CMD + " create roles -r demorealm -s name=manage-all -o");
out.println();
out.println("Create a new user in realm 'demorealm' returning only 'id', and 'username' attributes:");
out.println(" " + PROMPT + " " + CMD + " create users -r demorealm -s username=testuser -s enabled=true -o --fields id,username");
out.println();
out.println("Create a new client using configuration read from standard input:");
if (OS_ARCH.isWindows()) {
out.println(" " + PROMPT + " echo { \"clientId\": \"my_client\" } | " + CMD + " create clients -r demorealm -f -");
} else {
out.println(" " + PROMPT + " " + CMD + " create clients -r demorealm -f - << EOF");
out.println(" {");
out.println(" \"clientId\": \"my_client\"");
out.println(" }");
out.println(" EOF");
}
out.println();
out.println("Create a client using file as a template, and override some attributes - return an 'id' of new client:");
out.println(" " + PROMPT + " " + CMD + " create clients -r demorealm -f my_client.json -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -i");
out.println();
out.println("Create a new client role for client my_client in realm 'demorealm' (replace ID with output of previous example command):");
out.println(" " + PROMPT + " " + CMD + " create clients/ID/roles -r demorealm -s name=client_role");
out.println();
out.println();
out.println("Use '" + CMD + " help' for general information and a list of commands");
return sb.toString();
}
}

Some files were not shown because too many files have changed in this diff Show more