New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New Enterprise Module : JAAS Security #244
Changes from 1 commit
0420402
965afb9
190cfcd
93b20a8
29b8c58
30d6d44
06de29d
88bd658
f139000
0d10d94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,3 +16,4 @@ bin/ | |
hibernateDB | ||
springHibernateDB | ||
osgi/felix-cache/ | ||
.java-version |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
#hazelcast-security-examples | ||
|
||
## Introduction | ||
|
||
Sample code for Hazelcast Client Login Security implemented with JAAS. There are 3 classes. | ||
|
||
This example simulates a back-end security authorisation and authentication process for a client | ||
connecting into a Hazelcast cluster and manipulating a map. | ||
|
||
### Client | ||
|
||
Sets a UserNamePasswordCredentials class on the Client Config and then connects to the Member. | ||
|
||
In this class it will create 2 independent client connections to the running Member. One will connect as an admin user and perform | ||
a PUT operation on an "ImportantMap" and the second client connection will be set-up as a read-onluy user | ||
and try to perform the same PUT operation, this operation will throw an exception. | ||
|
||
### Member | ||
|
||
This is a Hazelcast Cluster member that is initialised with the _hazelcast.xml_ file | ||
|
||
Within the _hazelcast.xml_ we have defined some security properties. | ||
|
||
1. We have defined our own LoginModule to be executed when a client first connects. | ||
Called ClientLoginModule | ||
2. We have defined some permissions on the map _importantMap_ for 2 different groups, _readOnlyGroup_ and _adminGroup_. These groups | ||
are assigned to the client session in the LoginModule. You'll see that _adminGroup_ has PUT rights on the map whilst _readOnlyGroup_ does not. | ||
```XML | ||
<security enabled="true"> | ||
<client-login-modules> | ||
<login-module class-name="com.craftedbytes.hazelcast.security.ClientLoginModule" usage="required"> | ||
<properties> | ||
<property name="lookupFilePath">value3</property> | ||
</properties> | ||
</login-module> | ||
</client-login-modules> | ||
<client-permissions> | ||
<map-permission name="importantMap" principal="readOnlyGroup"> | ||
<actions> | ||
<action>create</action> | ||
<action>read</action> | ||
</actions> | ||
</map-permission> | ||
<map-permission name="importantMap" principal="adminGroup"> | ||
<actions> | ||
<action>create</action> | ||
<action>destroy</action> | ||
<action>put</action> | ||
<action>read</action> | ||
</actions> | ||
</map-permission> | ||
</client-permissions> | ||
</security> | ||
|
||
<map name="importantMap"/> | ||
``` | ||
Be sure to start this up with the Enterprise key as described in the Requirements section below. | ||
|
||
### ClientLoginModule | ||
|
||
This is executed on the Member when the Client connects. This class implements the javax.security.auth.spi.LoginModule | ||
|
||
This class is an example of what you should implement yourself to perform authentication operations | ||
against your security back-end of choice (KERBEROS, LDAP, ACTIVE DIRECTORY etc) | ||
|
||
|
||
|
||
|
||
## Requirements | ||
|
||
The examples requires an Enterprise Hazelcast key as we are using JAAS Security features that are only available in the Enterprise version of Hazelcast. You can obtain this key via an Enterprise Agreement or | ||
by using a 30 day trial key which can apply for here... | ||
|
||
https://hazelcast.com/hazelcast-enterprise-download/ | ||
|
||
Once you have the key you will need to start the MEMBER Java Process with the following VM switch... | ||
|
||
-Dhazelcast.enterprise.license.key=YOUR_ENTERPRISE_KEY_HERE | ||
|
||
## Further Reading | ||
|
||
It is recommended to read the following guide to Authentication using JAAS... | ||
|
||
http://www.javaranch.com/journal/2008/04/authentication-using-JAAS.html | ||
|
||
Also please take a look at the Hazelcast Documentation on JAAS Security... | ||
|
||
http://www.hazelcast.org/docs/latest/manual/html/nativeclientsecurity.html |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<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/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<artifactId>jaas-security</artifactId> | ||
<packaging>jar</packaging> | ||
|
||
<name>Enterprise - JAAS Security</name> | ||
<url>http://maven.apache.org</url> | ||
|
||
<parent> | ||
<artifactId>enterprise</artifactId> | ||
<groupId>com.hazelcast.samples.enterprise</groupId> | ||
<version>0.1-SNAPSHOT</version> | ||
<relativePath>../pom.xml</relativePath> | ||
</parent> | ||
|
||
<properties> | ||
<!-- needed for checkstyle/findbugs --> | ||
<main.basedir>${project.parent.parent.basedir}</main.basedir> | ||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
</properties> | ||
|
||
</project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import com.hazelcast.client.HazelcastClient; | ||
import com.hazelcast.client.config.ClientConfig; | ||
import com.hazelcast.core.HazelcastInstance; | ||
import com.hazelcast.logging.ILogger; | ||
import com.hazelcast.logging.Logger; | ||
import com.hazelcast.security.UsernamePasswordCredentials; | ||
|
||
import java.security.AccessControlException; | ||
import java.util.Map; | ||
import java.util.logging.Level; | ||
|
||
import static com.hazelcast.examples.helper.LicenseUtils.ENTERPRISE_LICENSE_KEY; | ||
|
||
/** | ||
* Created by dbrimley on 19/05/2014. | ||
*/ | ||
public class Client { | ||
|
||
private final ILogger logger = Logger.getLogger(getClass().getName()); | ||
|
||
public static void main(String args[]){ | ||
|
||
Client client = new Client(); | ||
|
||
client.adminUserCanPutIntoImportantMap(); | ||
|
||
client.readOnlyUserCannotPutIntoImportantMap(); | ||
|
||
|
||
} | ||
|
||
private void readOnlyUserCannotPutIntoImportantMap() { | ||
|
||
HazelcastInstance readOnlyClient = getClientConnection("chris", "password2", "127.0.0.1"); | ||
|
||
Map<String,String> readOnlyClientsImportantMap = readOnlyClient.getMap("importantMap"); | ||
|
||
// This will pass | ||
logger.log(Level.INFO,"Chris is performing get on the ImportantMap"); | ||
readOnlyClientsImportantMap.get("1"); | ||
|
||
// This will fail as chris is not a member of the admin group | ||
try{ | ||
logger.log(Level.INFO,"Chris is performing put on the ImportantMap"); | ||
readOnlyClientsImportantMap.put("2","2"); | ||
} catch (AccessControlException e){ | ||
logger.log(Level.SEVERE,"Could not perform put operation, access denied",e); | ||
} | ||
} | ||
|
||
private void adminUserCanPutIntoImportantMap() { | ||
|
||
HazelcastInstance adminClient = getClientConnection("david", "password1", "127.0.0.1"); | ||
|
||
Map<String,String> adminClientsImportantMap = adminClient.getMap("importantMap"); | ||
|
||
// This will pass | ||
logger.log(Level.INFO,"David is performing put on the ImportantMap"); | ||
adminClientsImportantMap.put("1","1"); | ||
} | ||
|
||
private HazelcastInstance getClientConnection(String username, String password, String thisClientIP) { | ||
|
||
ClientConfig clientConfig = new ClientConfig(); | ||
clientConfig.setLicenseKey(ENTERPRISE_LICENSE_KEY); | ||
clientConfig.setCredentials(new UsernamePasswordCredentials(username, password)); | ||
clientConfig.getCredentials().setEndpoint(thisClientIP); | ||
return HazelcastClient.newHazelcastClient(clientConfig); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import com.hazelcast.logging.ILogger; | ||
import com.hazelcast.logging.Logger; | ||
import com.hazelcast.nio.ObjectDataInput; | ||
import com.hazelcast.nio.ObjectDataOutput; | ||
import com.hazelcast.nio.serialization.DataSerializable; | ||
import com.hazelcast.security.*; | ||
|
||
import javax.security.auth.Subject; | ||
import javax.security.auth.callback.Callback; | ||
import javax.security.auth.callback.CallbackHandler; | ||
import javax.security.auth.login.LoginException; | ||
import javax.security.auth.spi.LoginModule; | ||
import java.io.IOException; | ||
import java.security.Principal; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.logging.Level; | ||
|
||
/** | ||
* An example ClientLoginModule that hard codes authorisation details in a pair of Maps. You could amend this class to | ||
* perform look up against an LDAP store that would then return a set of Groups for the User. | ||
* <p> | ||
* Obviously you would NEVER store passwords in clear text. | ||
* | ||
*/ | ||
public class ClientLoginModule implements LoginModule { | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
private final ILogger logger = Logger.getLogger(getClass().getName()); | ||
private Credentials credentials; | ||
private Subject subject; | ||
private CallbackHandler callbackHandler; | ||
private Map sharedState; | ||
private Map options; | ||
|
||
// The Group that the user is authorised for. | ||
private UserGroupCredentials userGroupCredentials; | ||
|
||
// Usernames and Password are stored here in clear text, obviously NEVER do this. | ||
private static Map<String,String> allowedUsersMap = new HashMap<String,String>(); | ||
|
||
// This map represents the userAssignedGroup(s) that a user would belong to, this would be the result from your LDAP store. | ||
private static Map<String,String> userGroups = new HashMap<String,String>(); | ||
|
||
static{ | ||
|
||
allowedUsersMap.put("david","password1"); | ||
allowedUsersMap.put("chris","password2"); | ||
|
||
userGroups.put("david","adminGroup"); | ||
userGroups.put("chris","readOnlyGroup"); | ||
|
||
} | ||
|
||
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { | ||
this.subject = subject; | ||
this.callbackHandler = callbackHandler; | ||
this.sharedState = sharedState; | ||
this.options = options; | ||
} | ||
|
||
/** | ||
* Login is called when this module is executed. | ||
* | ||
* @return is login successful | ||
* @throws LoginException | ||
*/ | ||
public boolean login() throws LoginException { | ||
boolean loginOk = false; | ||
|
||
final CredentialsCallback cb = new CredentialsCallback(); | ||
try { | ||
callbackHandler.handle(new Callback[]{cb}); | ||
credentials = cb.getCredentials(); | ||
} catch (Exception e) { | ||
throw new LoginException(e.getClass().getName() + ":" + e.getMessage()); | ||
} | ||
|
||
if(credentials == null) { | ||
logger.log(Level.WARNING, "Credentials could not be retrieved!"); | ||
return false; | ||
} | ||
logger.log(Level.INFO, "Authenticating " + SecurityUtil.getCredentialsFullName(credentials)); | ||
|
||
if (credentials instanceof UsernamePasswordCredentials){ | ||
loginOk = doLoginCheck((UsernamePasswordCredentials) credentials); | ||
} | ||
|
||
return loginOk; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When authentication fails, then Returning There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, will change. |
||
} | ||
|
||
private boolean doLoginCheck(UsernamePasswordCredentials credentials) { | ||
|
||
String username = credentials.getUsername(); | ||
String password = allowedUsersMap.get(username); | ||
boolean loginCheckOk = false; | ||
|
||
if (password != null){ | ||
if(password.equals(credentials.getPassword())){ | ||
String userGroup = userGroups.get(username); | ||
if (userGroup != null){ | ||
userGroupCredentials = new UserGroupCredentials(credentials.getEndpoint(),userGroup); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't add the authentication name part of the information into the Subject. Nonetheless, it's not a problem for this code sample. |
||
sharedState.put(SecurityConstants.ATTRIBUTE_CREDENTIALS, credentials); | ||
loginCheckOk = true; | ||
} else { | ||
logger.log(Level.WARNING, "User Group not found for user " + username); | ||
loginCheckOk = false; | ||
} | ||
} | ||
} else { | ||
logger.log(Level.WARNING, "User details not found for " + username); | ||
loginCheckOk = false; | ||
} | ||
|
||
return loginCheckOk; | ||
|
||
} | ||
|
||
/** | ||
* Commit is called when all of the modules in the chain have passed. | ||
* @return | ||
* @throws LoginException | ||
*/ | ||
public final boolean commit() throws LoginException { | ||
logger.log(Level.FINEST, "Committing authentication of " + SecurityUtil.getCredentialsFullName(credentials)); | ||
final Principal principal = new ClusterPrincipal(userGroupCredentials); | ||
subject.getPrincipals().add(principal); | ||
sharedState.put(SecurityConstants.ATTRIBUTE_PRINCIPAL, principal); | ||
return true; | ||
} | ||
|
||
/** | ||
* Abort is called when one of the modules in the chain has failed. | ||
* @return | ||
* @throws LoginException | ||
*/ | ||
public final boolean abort() throws LoginException { | ||
logger.log(Level.FINEST, "Aborting authentication of " + SecurityUtil.getCredentialsFullName(credentials)); | ||
clearSubject(); | ||
return true; | ||
} | ||
|
||
/** | ||
* Graceful Logout | ||
* | ||
* @return | ||
* @throws LoginException | ||
*/ | ||
public final boolean logout() throws LoginException { | ||
logger.log(Level.FINEST, "Logging out " + SecurityUtil.getCredentialsFullName(credentials)); | ||
clearSubject(); | ||
return true; | ||
} | ||
|
||
/** | ||
* Tidy up the Subject | ||
*/ | ||
private void clearSubject() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Login modules should only clean-up their stuff (i.e. avoid removing data introduced by other login modules). It's a minor thing in this case, but if users use code samples as a quick-start for their implementations this should be done right. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I only clean up if the whole chain is aborted or if there is a logout event, so I think it's fine. |
||
subject.getPrincipals().clear(); | ||
subject.getPrivateCredentials().clear(); | ||
subject.getPublicCredentials().clear(); | ||
} | ||
|
||
public class UserGroupCredentials implements Credentials, DataSerializable { | ||
|
||
private String endpoint; | ||
private String userGroup; | ||
|
||
public UserGroupCredentials(){} | ||
|
||
public UserGroupCredentials(String endPoint, String userGroup) { | ||
this.endpoint = endPoint; | ||
this.userGroup = userGroup; | ||
} | ||
|
||
public String getEndpoint() { | ||
return this.endpoint; | ||
} | ||
|
||
public void setEndpoint(String endpoint) { | ||
this.endpoint = endpoint; | ||
} | ||
|
||
public String getPrincipal() { | ||
return this.userGroup; | ||
} | ||
|
||
public void writeData(ObjectDataOutput objectDataOutput) throws IOException { | ||
objectDataOutput.writeUTF(endpoint); | ||
objectDataOutput.writeUTF(userGroup); | ||
} | ||
|
||
public void readData(ObjectDataInput objectDataInput) throws IOException { | ||
this.endpoint = objectDataInput.readUTF(); | ||
this.userGroup = objectDataInput.readUTF(); | ||
} | ||
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a more descriptive Javadoc.