Skip to content
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

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -16,3 +16,4 @@ bin/
hibernateDB
springHibernateDB
osgi/felix-cache/
.java-version
88 changes: 88 additions & 0 deletions enterprise/jaas-security/README.md
@@ -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
24 changes: 24 additions & 0 deletions enterprise/jaas-security/pom.xml
@@ -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>
70 changes: 70 additions & 0 deletions enterprise/jaas-security/src/main/java/Client.java
@@ -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.
Copy link
Member

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.

*/
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);
}
}
198 changes: 198 additions & 0 deletions enterprise/jaas-security/src/main/java/ClientLoginModule.java
@@ -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 {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest this impl to be reviewed by @tkountis @kwart

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When authentication fails, then LoginException should be thrown. The FailedLoginException child class is probably the best candidate.

Returning false just means the login module should be ignored. It can be used for instance when the login module stack contains several modules, each accepting different parameters. Return false in case the actual parameters are not sufficient for current login module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The 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() {
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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();
}

}
}