Improve OOTB experience for ssh and jmx authentication in fuse

This commit is contained in:
mposolda 2015-01-23 20:40:38 +01:00
parent 71b36ceb07
commit ee4fbca868
12 changed files with 119 additions and 32 deletions

View file

@ -9,7 +9,7 @@ import org.keycloak.connections.mongo.api.types.MapperRegistry;
import org.keycloak.connections.mongo.impl.EntityInfo; import org.keycloak.connections.mongo.impl.EntityInfo;
import org.keycloak.connections.mongo.impl.MongoStoreImpl; import org.keycloak.connections.mongo.impl.MongoStoreImpl;
import org.keycloak.models.utils.reflection.Property; import org.keycloak.models.utils.reflection.Property;
import org.keycloak.models.utils.reflection.Types; import org.keycloak.util.reflections.Types;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;

View file

@ -1,4 +1,4 @@
package org.keycloak.models.utils.reflection; package org.keycloak.util.reflections;
import java.beans.Introspector; import java.beans.Introspector;
import java.io.Serializable; import java.io.Serializable;

View file

@ -1,4 +1,4 @@
package org.keycloak.models.utils.reflection; package org.keycloak.util.reflections;
import java.lang.reflect.AccessibleObject; import java.lang.reflect.AccessibleObject;
import java.security.PrivilegedAction; import java.security.PrivilegedAction;

View file

@ -1,4 +1,4 @@
package org.keycloak.models.utils.reflection; package org.keycloak.util.reflections;
import java.lang.reflect.Type; import java.lang.reflect.Type;

View file

@ -10,7 +10,10 @@
<cm:property-placeholder persistent-id="org.keycloak" update-strategy="reload"> <cm:property-placeholder persistent-id="org.keycloak" update-strategy="reload">
<cm:default-properties> <cm:default-properties>
<cm:property name="jaasBearerKeycloakConfigFile" value="$[karaf.base]/etc/keycloak-hawtio.json"/> <cm:property name="jaasBearerKeycloakConfigFile" value="$[karaf.base]/etc/keycloak-hawtio.json"/>
<cm:property name="jaasBearerRolePrincipalClass" value=""/>
<cm:property name="jaasDirectAccessKeycloakConfigFile" value="$[karaf.base]/etc/keycloak-direct-access.json"/> <cm:property name="jaasDirectAccessKeycloakConfigFile" value="$[karaf.base]/etc/keycloak-direct-access.json"/>
<cm:property name="jaasDirectAccessRolePrincipalClass" value="org.apache.karaf.jaas.boot.principal.RolePrincipal"/>
</cm:default-properties> </cm:default-properties>
</cm:property-placeholder> </cm:property-placeholder>
@ -20,12 +23,14 @@
<jaas:module className="org.keycloak.adapters.jaas.BearerTokenLoginModule" <jaas:module className="org.keycloak.adapters.jaas.BearerTokenLoginModule"
flags="sufficient"> flags="sufficient">
keycloak-config-file = ${jaasBearerKeycloakConfigFile} keycloak-config-file = ${jaasBearerKeycloakConfigFile}
role-principal-class = ${jaasBearerRolePrincipalClass}
</jaas:module> </jaas:module>
<!-- Used for direct username/password authentication for non-web scenarios (like ssh) --> <!-- Used for direct username/password authentication for non-web scenarios (like ssh) -->
<jaas:module className="org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule" <jaas:module className="org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule"
flags="sufficient"> flags="sufficient">
keycloak-config-file = ${jaasDirectAccessKeycloakConfigFile} keycloak-config-file = ${jaasDirectAccessKeycloakConfigFile}
role-principal-class = ${jaasDirectAccessRolePrincipalClass}
</jaas:module> </jaas:module>
</jaas:config> </jaas:config>

View file

@ -35,6 +35,16 @@
Security for <ulink url="http://cxf.apache.org/">Apache CXF</ulink> endpoints running on default engine provided by CXF servlet. Security for <ulink url="http://cxf.apache.org/">Apache CXF</ulink> endpoints running on default engine provided by CXF servlet.
</para> </para>
</listitem> </listitem>
<listitem>
<para>
Security for SSH and JMX admin access.
</para>
</listitem>
<listitem>
<para>
Security for <ulink url="http://hawt.io/">Hawt.io admin console</ulink> .
</para>
</listitem>
</itemizedlist> </itemizedlist>
</para> </para>
<para>The best place to start is look at Fuse demo bundled as part of Keycloak examples in directory <literal>examples/fuse</literal> .</para> <para>The best place to start is look at Fuse demo bundled as part of Keycloak examples in directory <literal>examples/fuse</literal> .</para>

View file

@ -31,7 +31,12 @@
</variablelist> </variablelist>
</para> </para>
<para> <para>
Both login modules have single configuration property <literal>keycloak-config-file</literal> where you need to provide location of keycloak.json configuration file. Both login modules have configuration property <literal>keycloak-config-file</literal> where you need to provide location of keycloak.json configuration file.
It could be either provided from filesystem or from classpath (in that case you may need value like <literal>classpath:/folder-on-classpath/keycloak.json</literal> ). It could be either provided from filesystem or from classpath (in that case you may need value like <literal>classpath:/folder-on-classpath/keycloak.json</literal> ).
</para> </para>
<para>
Second property <literal>role-principal-class</literal> allows to specify alternative class for Role principals attached to JAAS Subject. Default
value for Role principal is <literal>org.keycloak.adapters.jaas.RolePrincipal</literal> . Note that class should have constructor
with single String argument.
</para>
</section> </section>

View file

@ -15,14 +15,13 @@ allows to remotely connect to Keycloak and verify credentials based on [Direct a
Example steps for enable SSH authentication: Example steps for enable SSH authentication:
1) Import 'demo' realm as mentioned in [Base steps](../README.md#base-steps) . It contains client `sh-jmx-admin-client`, which is used for SSH authentication. 1) Import 'demo' realm as mentioned in [Base steps](../README.md#base-steps) . It contains client `ssh-jmx-admin-client`, which is used for SSH authentication.
Skip this step if you installed demo already. Skip this step if you installed demo already.
2) Then you need to update/specify these 2 properties in file `$FUSE_HOME/etc/org.apache.karaf.shell.cfg`: 2) Then you need to update/specify this property in file `$FUSE_HOME/etc/org.apache.karaf.shell.cfg`:
``` ```
sshRealm=keycloak sshRealm=keycloak
sshRole=org.keycloak.adapters.jaas.RolePrincipal:admin
``` ```
3) Copy file from Keycloak fuse examples `examples/fuse/fuse-admin/keycloak-direct-access.json` to `$FUSE_HOME/etc/` directory. 3) Copy file from Keycloak fuse examples `examples/fuse/fuse-admin/keycloak-direct-access.json` to `$FUSE_HOME/etc/` directory.
@ -30,7 +29,7 @@ This file contains configuration of the client application, which is used by JAA
4) Start Fuse and install `keycloak` JAAS realm into Fuse. This could be done easily by installing `keycloak-jaas` feature, which has JAAS realm predefined 4) Start Fuse and install `keycloak` JAAS realm into Fuse. This could be done easily by installing `keycloak-jaas` feature, which has JAAS realm predefined
(you are able to override it by using your own `keycloak` JAAS realm with higher ranking). As long as you already installed `keycloak-fuse-example` feature as mentioned (you are able to override it by using your own `keycloak` JAAS realm with higher ranking). As long as you already installed `keycloak-fuse-example` feature as mentioned
in [examples readme](../README.md), you can skip this step as `keycloak-jaas` is installed already. Otherwise use those commands: in [examples readme](../README.md), you can skip this step as `keycloak-jaas` is installed already. Otherwise use those commands (replace Keycloak version with current one):
``` ```
features:addurl mvn:org.keycloak/keycloak-osgi-features/1.1.0.Final/xml/features features:addurl mvn:org.keycloak/keycloak-osgi-features/1.1.0.Final/xml/features
@ -49,7 +48,7 @@ And login with password `password` . Note that other users from "demo" realm lik
JMX authentication with keycloak credentials on JBoss Fuse 6.1 JMX authentication with keycloak credentials on JBoss Fuse 6.1
-------------------------------------------------------------- --------------------------------------------------------------
This may be needed just in case if you really want to use jconsole or other external tool to perform remote connection to JMX through RMI. Otherwise it may This may be needed in case if you really want to use jconsole or other external tool to perform remote connection to JMX through RMI. Otherwise it may
be better to use just hawt.io/jolokia as jolokia agent is installed in hawt.io by default. be better to use just hawt.io/jolokia as jolokia agent is installed in hawt.io by default.
1) In file `$FUSE_HOME/etc/org.apache.karaf.management.cfg` you can change these 2 properties: 1) In file `$FUSE_HOME/etc/org.apache.karaf.management.cfg` you can change these 2 properties:
@ -72,30 +71,23 @@ may be still able to access MBeans remotely via HTTP (Hawtio). So make sure to p
really protect JMX mbeans. really protect JMX mbeans.
JMX on JBoss Fuse 6.2 and Apache Karaf 3.0.2 SSH and JMX on JBoss Fuse 6.2 and Apache Karaf 3.0.2
-------------------------------------------- --------------------------------------------
For SSH steps are very similar to above for 6.1. In JBoss Fuse 6.2 you may need to install `ssh` feature as it doesn't seem to be installed here by default.
For JMX, the steps are similar like above, however there is more fine grained authorization for JMX access in Fuse 6.2. You still need to configure jmxRealm in ```
`$FUSE_HOME/etc/org.apache.karaf.management.cfg`, however jmxRole is not needed. So in config should be just this: features:install ssh
```
For JMX, the steps are similar like for Fuse 6.1, however there is more fine grained authorization for JMX access in Fuse 6.2 and Karaf 3.
You need to install just jmxRealm in `$FUSE_HOME/etc/org.apache.karaf.management.cfg` . Property `jmxRole` is no longer valid.
``` ```
jmxRealm=keycloak jmxRealm=keycloak
``` ```
And then you need to configure authorization for all specific MBeans, which you want to access with authenticated user. For generic access
you can configure the most generic ACL in file `$FUSE_HOME/etc/jmx.acl.cfg` like this:
``` Actually if you login as user `admin`, you have very limited privileges without possibility to do much JMX operations as this user has just `admin` role, which is not allowed to do much in JMX.
list* = org.keycloak.adapters.jaas.RolePrincipal:admin
get* = org.keycloak.adapters.jaas.RolePrincipal:admin However if you login as user `jmxadmin` with password `password`, you will have all JMX privileges! This user has composite role `jmxAdmin`, which is mapped to
is* = org.keycloak.adapters.jaas.RolePrincipal:admin all possible roles used in JMX authorization files like `etc/jmx.acl.*.cfg` . See karaf documentation for more info about fine grained JMX authorization.
set* = org.keycloak.adapters.jaas.RolePrincipal:admin
* = org.keycloak.adapters.jaas.RolePrincipal:admin
```
Then admin will have permissions to see all the attributes and trigger all JMX operations, which don't have their own ACL. You can configure
those specific ACLs too. For example if you want to allow operation `lookupAgents` in MBean `jolokia:type=Discovery` you can configure
in file `$FUSE_HOME/etc/jmx.acl.jolokia.Discovery.cfg` the property like this:
```
lookupAgents = org.keycloak.adapters.jaas.RolePrincipal:admin
```

View file

@ -72,6 +72,22 @@
"applicationRoles": { "applicationRoles": {
"realm-management": [ "realm-admin" ] "realm-management": [ "realm-admin" ]
} }
},
{
"username" : "jmxadmin",
"enabled": true,
"email" : "jmxadmin@admin.com",
"firstName": "JmxAdmin",
"lastName": "Burke",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"realmRoles": [ "user", "jmxAdmin" ],
"applicationRoles": {
"account": [ "manage-account" ],
"realm-management": [ "realm-admin" ]
}
} }
], ],
"roles" : { "roles" : {
@ -83,6 +99,38 @@
{ {
"name": "admin", "name": "admin",
"description": "Administrator privileges" "description": "Administrator privileges"
},
{
"name": "manager"
},
{
"name": "viewer"
},
{
"name": "Operator"
},
{
"name": "Maintainer"
},
{
"name": "Deployer"
},
{
"name": "Auditor"
},
{
"name": "Administrator"
},
{
"name": "SuperUser"
},
{
"name": "jmxAdmin",
"description": "Admin role with all privileges to SSH and JMX access",
"composite": true,
"composites": {
"realm": [ "admin", "manager", "viewer", "Operator", "Maintainer", "Deployer", "Auditor", "Administrator", "SuperUser" ]
}
} }
] ]
}, },
@ -149,7 +197,7 @@
"scopeMappings": [ "scopeMappings": [
{ {
"client": "ssh-jmx-admin-client", "client": "ssh-jmx-admin-client",
"roles": ["admin"] "roles": [ "admin", "jmxAdmin" ]
} }
] ]
} }

View file

@ -1,6 +1,7 @@
package org.keycloak.adapters.jaas; package org.keycloak.adapters.jaas;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.security.Principal; import java.security.Principal;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
@ -28,6 +29,7 @@ import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.util.reflections.Reflections;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -35,11 +37,13 @@ import org.keycloak.representations.AccessToken;
public abstract class AbstractKeycloakLoginModule implements LoginModule { public abstract class AbstractKeycloakLoginModule implements LoginModule {
public static final String KEYCLOAK_CONFIG_FILE_OPTION = "keycloak-config-file"; public static final String KEYCLOAK_CONFIG_FILE_OPTION = "keycloak-config-file";
public static final String ROLE_PRINCIPAL_CLASS_OPTION = "role-principal-class";
protected Subject subject; protected Subject subject;
protected CallbackHandler callbackHandler; protected CallbackHandler callbackHandler;
protected Auth auth; protected Auth auth;
protected KeycloakDeployment deployment; protected KeycloakDeployment deployment;
protected String rolePrincipalClass;
// This is to avoid parsing keycloak.json file in each request. Key is file location, Value is parsed keycloak deployment // This is to avoid parsing keycloak.json file in each request. Key is file location, Value is parsed keycloak deployment
private static ConcurrentMap<String, KeycloakDeployment> deployments = new ConcurrentHashMap<String, KeycloakDeployment>(); private static ConcurrentMap<String, KeycloakDeployment> deployments = new ConcurrentHashMap<String, KeycloakDeployment>();
@ -50,6 +54,9 @@ public abstract class AbstractKeycloakLoginModule implements LoginModule {
this.callbackHandler = callbackHandler; this.callbackHandler = callbackHandler;
String configFile = (String)options.get(KEYCLOAK_CONFIG_FILE_OPTION); String configFile = (String)options.get(KEYCLOAK_CONFIG_FILE_OPTION);
rolePrincipalClass = (String)options.get(ROLE_PRINCIPAL_CLASS_OPTION);
getLogger().debug("Declared options: " + KEYCLOAK_CONFIG_FILE_OPTION + "=" + configFile + ", " + ROLE_PRINCIPAL_CLASS_OPTION + "=" + rolePrincipalClass);
if (configFile != null) { if (configFile != null) {
deployment = deployments.get(configFile); deployment = deployments.get(configFile);
if (deployment == null) { if (deployment == null) {
@ -117,7 +124,7 @@ public abstract class AbstractKeycloakLoginModule implements LoginModule {
this.subject.getPrivateCredentials().add(auth.getTokenString()); this.subject.getPrivateCredentials().add(auth.getTokenString());
if (auth.getRoles() != null) { if (auth.getRoles() != null) {
for (String roleName : auth.getRoles()) { for (String roleName : auth.getRoles()) {
RolePrincipal rolePrinc = new RolePrincipal(roleName); Principal rolePrinc = createRolePrincipal(roleName);
this.subject.getPrincipals().add(rolePrinc); this.subject.getPrincipals().add(rolePrinc);
} }
} }
@ -125,6 +132,23 @@ public abstract class AbstractKeycloakLoginModule implements LoginModule {
return true; return true;
} }
protected Principal createRolePrincipal(String roleName) {
if (rolePrincipalClass != null && rolePrincipalClass.length() > 0) {
try {
Class<Principal> clazz = Reflections.classForName(rolePrincipalClass, getClass().getClassLoader());
Constructor<Principal> constructor = clazz.getDeclaredConstructor(String.class);
return constructor.newInstance(roleName);
} catch (Exception e) {
getLogger().warn("Unable to create declared roleClass " + rolePrincipalClass + " due to " + e.getMessage());
}
}
// Fallback to default rolePrincipal class
return new RolePrincipal(roleName);
}
@Override @Override
public boolean abort() throws LoginException { public boolean abort() throws LoginException {
return true; return true;

View file

@ -118,6 +118,7 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter {
started = true; started = true;
} }
// TODO: Use 'Reflections.classForName'
protected Class<? extends KeycloakConfigResolver> loadResolverClass() { protected Class<? extends KeycloakConfigResolver> loadResolverClass() {
try { try {
return (Class<? extends KeycloakConfigResolver>)getClass().getClassLoader().loadClass(keycloakConfigResolverClass); return (Class<? extends KeycloakConfigResolver>)getClass().getClassLoader().loadClass(keycloakConfigResolverClass);

View file

@ -6,6 +6,8 @@ import java.lang.reflect.Member;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import org.keycloak.util.reflections.Reflections;
/** /**
* A bean property based on the value represented by a getter/setter method pair * A bean property based on the value represented by a getter/setter method pair
*/ */