This example demonstrates the basics of creating a custom SecurityManager with an implementation of the SecurityManager.authenticate and SecurityManager.authorize methods that connect to a Java client.

Note: The security implementation of every installation is unique. This example should not be used in a production environment. The purpose of this example is to provide a basic example that can be adapted to each user's own security needs.

In this example, you will:

  1. Create a list of approved users to authenticate and authorize against.
  2. Create a BasicSecurityManager implementation that uses the SecurityManager authenticate and authorize methods.
  3. Start a GemFire cluster with the BasicSecurityManager security manager.
  4. Create a Java client application that authenticates against the application and PUTS and GETS data into a region.

Create a List of Approved Users

GemFire offers multiple layers of access to a GemFire cluster, which are defined by the GemFire resource permissions. For more information about GemFire resource permissions, see Resource Permissions in Implementing Authorization.

In this example, you create a USER class and initialize two users:

  • An Operator user that can manage the GemFire cluster but has no data access.
  • An Application Developer who can access the GemFire cluster to manager data, but cannot manage the GemFire cluster to perform tasks like deleting the cluster.

Create the USER class as follows.

Warning: You must implement the Serializable interface. This allows GemFire to deserialize the class when checking the client username.

import java.io.Serializable;
import java.util.List;

import org.apache.geode.security.ResourcePermission;

public class User implements Serializable {

  List<ResourcePermission> userPermissions;
  String userName;
  String userPassword;
  
  public User(String userName, String userPassword, List<ResourcePermission> userPermissions) {
    this.userName = userName;
    this.userPassword = userPassword;
    this.userPermissions = userPermissions;
  }
  
  public String getUserPassword() {
    return userPassword;
  }
  
  @Override
  public String toString() {
    return userName;
  }
  
  public List<ResourcePermission> getPermissions() {
    return this.userPermissions;
  }
  
  public boolean hasPermission(ResourcePermission resourcePermissionRequested) {
    boolean hasPermission = false;
    
    for (ResourcePermission userPermission : userPermissions) {
      if (userPermission.implies(resourcePermissionRequested)) {
        hasPermission = true;
        break;
      }
    }
    return hasPermission;
  }
}

Create a BasicSecurityManager

  1. Create a BasicSecurityManager that implements the GemFire SecurityManager interface. In the init method of the BasicSecurityManager two USERS are created: operator and appdeveloper.

    Note: In this example the username and password are hard-coded when creating the Users. This does not represent best practices.

    In your application, this list may come from an external system.

    In the BasicSecurityManager:

    • Create an Operator user with CLUSTER MANAGE, CLUSTER WRITE, and CLUSTER READ permissions. Set the username to "operator" and the password to "secret".

    • Create an Application Developer user with CLUSTER READ, DATA MANAGE, DATA WRITE, DATA READ. Set the username to "appDeveloper" and the password to "NotSoSecret".

    • Add the users to an "approved users" list, which will allow the application to check incoming credentials.

    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Properties;
    
    import org.apache.geode.security.AuthenticationFailedException;
    import org.apache.geode.security.ResourcePermission;
    import org.apache.geode.security.SecurityManager;
    
    public class BasicSecurityManager implements SecurityManager {
    
      private HashMap<String, User> approvedUsersList = new HashMap<>();
    
      @Override
      public void init(final Properties securityProperties) {
    
        List<ResourcePermission> operatorPermissions = new ArrayList<>();
        operatorPermissions.add(new ResourcePermission(ResourcePermission.Resource.CLUSTER,
                ResourcePermission.Operation.MANAGE));
        operatorPermissions.add(new ResourcePermission(ResourcePermission.Resource.CLUSTER,
                ResourcePermission.Operation.WRITE));
        operatorPermissions.add(new ResourcePermission(ResourcePermission.Resource.CLUSTER,
                ResourcePermission.Operation.READ));
    
        User operator = new User("operator", "secret", operatorPermissions);
    
        List<ResourcePermission> appDevPermissions = new ArrayList<>();
        appDevPermissions.add(new ResourcePermission(ResourcePermission.Resource.CLUSTER,
                ResourcePermission.Operation.READ));
        appDevPermissions.add(new ResourcePermission(ResourcePermission.Resource.DATA,
                ResourcePermission.Operation.MANAGE));
        appDevPermissions.add(new ResourcePermission(ResourcePermission.Resource.DATA,
                ResourcePermission.Operation.WRITE));
        appDevPermissions.add(new ResourcePermission(ResourcePermission.Resource.DATA,
                ResourcePermission.Operation.READ));
    
        User appDeveloper = new User("appDeveloper", "NotSoSecret", appDevPermissions);
    
        this.approvedUsersList.put("operator", operator);
        this.approvedUsersList.put("appDeveloper", appDeveloper);
    
      }
    }
    
  2. Implement the authentication and authorization methods.

Authentication

In the authenticate method, you must check the credentials passed into the BasicSecurityManager from the client against those in the approvedUsers list. If the credentials match, the method returns a USER object, authenticatedUser. This is the object is passed into the authorize method when the client application performs an operation.

public class BasicSecurityManager implements SecurityManager {
  
  private HashMap<String, User> approvedUsersList = new HashMap<>();
  
  @Override
  public void init(final Properties securityProperties) {...}
  
  @Override
  public Object authenticate(Properties credentials) throws AuthenticationFailedException {
  
    String usernamePassedIn = credentials.getProperty(USER_NAME);
    String passwordPassedIn = credentials.getProperty(PASSWORD);
    
    User authenticatedUser = this.approvedUsersList.get(usernamePassedIn);
    
      if (authenticatedUser == null) {
         throw new AuthenticationFailedException("Wrong username/password");
      }
      
      if (authenticatedUser != null && !authenticatedUser.getUserPassword().equals(passwordPassedIn)
              && !"".equals(authenticatedUser)) {
         throw new AuthenticationFailedException("Wrong username/password");
      }
      
    return authenticatedUser;
  }
}

Authorization

The object returned from the authenticate method above (the authenticatedUser object from above) is passed into the authorize method as the Object principal. This object is used to authorize the action that the client is attempting to perform.

The resourcePermissionRequested (the action the client wants to perform) is compared with the USERS given permissions that were defined when creating the two users in the init method. If the user is allowed to perform the requested action, then the method returns true. Otherwise, the method returns false and the action is denied.

public class BasicSecurityManager implements SecurityManager {
  
  private HashMap<String, User> approvedUsersList = new HashMap<>();
  
  @Override
  public void init(final Properties securityProperties) {...}
  
  @Override
  public Object authenticate(Properties credentials) throws AuthenticationFailedException {...}
  
  @Override
  public boolean authorize(Object principal, ResourcePermission resourcePermissionRequested) {
    
    if (principal == null) {
       return false;
    }
    
    User user = this.approvedUsersList.get(principal.toString());
    
      if (user == null) {
         return false;
      }
      
      for (ResourcePermission userPermission : user.getPermissions()) {
        if (userPermission.implies(resourcePermissionRequested)) {
           return true;
        }
    }
    
    return false;
  }
}

Starting a GemFire Cluster with the Security Manager

You must add your BasicSecurityManager to the classpath when starting a GemFire cluster.

  1. Build the .jar file for the BasicSecurityManager created above. Record the directory and file path where the jar file is created for use in a later step.

  2. On a command line, run gfsh to start GemFire's shell.

  3. Start a locator and include the path to the jar file and class name of the BasicSecurityManager. The start locator command will look like this:

    start locator --name=locator1 --J=-Dgemfire.securitymanager=BasicSecurityManager --classpath=/path/to/your/jar/file/BasicSecurityManager-1.0-SNAPSHOT.jar
    

    Where:

    • --J=-Dgemfire.securitymanager=BasicSecurityManager defines the package and class for your security manager and allows GemFire to find the class when starting.
    • --classpath=[path to your jar file]/BasicSecurityManager-1.0-SNAPSHOT.jar defines the path to the jar file that GemFire should use as the security manager.
  4. Once the locator has started, you will see output similar to this:

    Starting a Geode Locator in [path to where GemFire was started] /locator1...
    
    .........
    
    Locator in [path to where GemFire was started]/locator1 on [ip address] [10334] as locator1 is currently online.
    Process ID: 75033
    Uptime: 11 seconds
    Geode Version: 1.15.0-build.0
    Java Version: 1.8.0_292
    ...
    
    Unable to auto-connect (Security Manager may be enabled). Please use "connect --locator=[ip address] [10334] --user --password" to connect Gfsh to the locator.
    
    Authentication required to connect to the Manager.
    

    This output shows that locator started. When the security manager is included to start the cluster, it is immediately used to authenticate the current user.

  5. Connect to the cluster as the Operator as defined in the BasicSecurityManager class above. This is the only role created for this example that has the authorization to manage the cluster.

    In gfsh, the command would look similar to the following:

    gfsh: connect --locator= [IP Address that GemFire is running on] [10334] --user=operator --password=secret
    

    You should now be connected to the locator.

  6. Start a server. This will be very similar to starting the locator. In gfsh, run the start server command, which will include the path to the same BasicSecurityManager.jar file used when starting the locator.

    gfsh: start server --name=server1 --locators=localhost[10334] --classpath=[path to your security manager]/BasicSecurityManager-1.0-SNAPSHOT.jar --user=operator --password=secret
    

    Repeat this step for each server you need to start, but change the server --name= parameter to be unique for each server.

You now have a GemFire cluster running with your BasicSecurityManager.


Create a Java Client application

To create a Java client application that authenticates against the application and PUTS and GETS data into a region:

  • You must create a region on the GemFire cluster for the application to interact with.

  • The client application must have a class that implements the AuthInitialize interface. This class is used by GemFire to provide the credentials to the cluster. The client application must set its credentials composed of two properties,security username and security-password.

  • The client application must set the security-client-auth-init property, which indicates to GemFire the class that implements the AuthInitialize interface.

Create a Region on the GemFire Cluster

Create a region on the GemFire cluster for the application to interact with.

At this point in our example, gfsh is connected as the Operator who has permission to manage the cluster, but does not have permission to interact with or manage data. To create a region, you must disconnect as Operator, connect as appDeveloper, then create the region.

  1. In gfsh, disconnect from being the Operator by running:

    gfsh: disconnect
    
  2. In gfsh, connect as the appDeveloper by running:

    gfsh: connect –-user=appDevleoper –-password=NotSoSecret
    
  3. In gfsh, create a region named helloWorld by running the command below. This will create a partitioned region in your GemFire cluster from which you can PUT and GET data. For more information about partitioned regions, see Partitioned Regions.

    gfsh: create region --name=helloWorld --type=PARTITION
    

Set Credential Properties

You must set the security-username and security-password in the class that implements the AuthInitialize interface. These credentials might come from an external source, such as a credentials database, ActiveDirectory, or some other external system.

Warning: The goal of this example is to demonstrate how the security manager works. To simplify the example, the credentials are hard-coded into the application. This is not a best practice for security.

  1. Create the class UserPasswordAuthInit that implements the AuthInitialize interface.

    import java.util.Properties;
    import org.apache.geode.distributed.DistributedMember;
    import org.apache.geode.security.AuthInitialize;
    import org.apache.geode.security.AuthenticationFailedException;
    
    public class UserPasswordAuthInit implements AuthInitialize {
    
      @Override
      public Properties getCredentials(Properties properties, DistributedMember distributedMember, boolean isPeer) throws AuthenticationFailedException {
        properties.setProperty("security-username", "appDeveloper");
        properties.setProperty("security-password", "NotSoSecret");
        return properties;
      }
    }
    

    This basic class sets two properties, security-username and security-password, that match the credentials declared for the appDeveloper user in the BasicSecurityManager class.

  2. Set set the security-client-auth-init property in the Main class and pass it to the ClientCacheFactory.

    import java.util.Properties;
    import org.apache.geode.cache.Region;
    import org.apache.geode.cache.client.ClientCache;
    import org.apache.geode.cache.client.ClientCacheFactory;
    import org.apache.geode.cache.client.ClientRegionShortcut;
    
    public class Main {
    
      public static void main (String[] args) {
        Properties properties = new Properties();
        properties.setProperty("security-client-auth-init", UserPasswordAuthInit.class.getName());
    
        ClientCache cache = new ClientCacheFactory(properties).addPoolLocator("localhost", 10334).create();
    
        Region<String, String>
        helloWorldRegion = cache.<String, String>createClientRegionFactory(ClientRegionShortcut.PROXY).create("helloWorld");
    
        helloWorldRegion.put("1", "HelloWorldValue");
        String value1 = helloWorldRegion.get("1");
        System.out.println(value1);
        cache.close();
      }
    }
    
  3. Run the application. You should see output similar to the following:

    ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console...
    SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
    SLF4J: Defaulting to no-operation (NOP) logger implementation
    SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
    
    HelloWorldValue
    
    Process finished with exit code 0
    

    This outputs the value "HelloWorldValue" that you put in for key 1. If the output includes an authentication error, confirm that you have the correct username and password in your AuthInitialize class. Ignore other ERROR messages for this example.


Client Authorization Error

If you were to remove the appDevelopers permission to WRITE to the GemFire cluster, you would see an error similar to the following:

ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console...
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Exception in thread "main" org.apache.geode.cache.client.ServerOperationException: remote server on [your IP address]: org.apache.geode.security.NotAuthorizedException: appDeveloper not authorized for DATA:WRITE:helloWorld:1
  at org.apache.geode.cache.client.internal.OpExecutorImpl.handleException(OpExecutorImpl.java:554)
  at org.apache.geode.cache.client.internal.OpExecutorImpl.handleException(OpExecutorImpl.java:615)
  at org.apache.geode.cache.client.internal.OpExecutorImpl.handleException(OpExecutorImpl.java:501)
  at org.apache.geode.cache.client.internal.OpExecutorImpl.execute(OpExecutorImpl.java:142)
  at org.apache.geode.cache.client.internal.OpExecutorImpl.execute(OpExecutorImpl.java:108)
  at org.apache.geode.cache.client.internal.PoolImpl.execute(PoolImpl.java:776)
  at org.apache.geode.cache.client.internal.PutOp.execute(PutOp.java:91)
  at org.apache.geode.cache.client.internal.ServerRegionProxy.put(ServerRegionProxy.java:159)
  at org.apache.geode.internal.cache.LocalRegion.serverPut(LocalRegion.java:3048)
  at org.apache.geode.internal.cache.LocalRegion.cacheWriteBeforePut(LocalRegion.java:3165)
  at org.apache.geode.internal.cache.ProxyRegionMap.basicPut(ProxyRegionMap.java:238)
  at org.apache.geode.internal.cache.LocalRegion.virtualPut(LocalRegion.java:5613)
  at org.apache.geode.internal.cache.LocalRegion.virtualPut(LocalRegion.java:5591)
  at org.apache.geode.internal.cache.LocalRegionDataView.putEntry(LocalRegionDataView.java:156)
  at org.apache.geode.internal.cache.LocalRegion.basicPut(LocalRegion.java:5049)
  at org.apache.geode.internal.cache.LocalRegion.validatedPut(LocalRegion.java:1648)
  at org.apache.geode.internal.cache.LocalRegion.put(LocalRegion.java:1635)
  at org.apache.geode.internal.cache.AbstractRegion.put(AbstractRegion.java:442)
  at Main.main(Main.java:21)
Caused by: org.apache.geode.security.NotAuthorizedException: appDeveloper not authorized for DATA:WRITE:helloWorld:1
  at org.apache.geode.internal.security.IntegratedSecurityService.authorize(IntegratedSecurityService.java:292)
  at org.apache.geode.internal.security.IntegratedSecurityService.authorize(IntegratedSecurityService.java:275)
  at org.apache.geode.internal.security.IntegratedSecurityService.authorize(IntegratedSecurityService.java:269)
  at org.apache.geode.internal.cache.tier.sockets.command.Put70.cmdExecute(Put70.java:246)
  at org.apache.geode.internal.cache.tier.sockets.BaseCommand.execute(BaseCommand.java:187)
  at org.apache.geode.internal.cache.tier.sockets.ServerConnection.doNormalMessage(ServerConnection.java:881)
  at org.apache.geode.internal.cache.tier.sockets.ServerConnection.doOneMessage(ServerConnection.java:1070)
  at org.apache.geode.internal.cache.tier.sockets.ServerConnection.run(ServerConnection.java:1344)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  at org.apache.geode.internal.cache.tier.sockets.AcceptorImpl.lambda$initializeServerConnectionThreadPool$3(AcceptorImpl.java:690)
  at org.apache.geode.logging.internal.executors.LoggingThreadFactory.lambda$newThread$0(LoggingThreadFactory.java:120)
  at java.lang.Thread.run(Thread.java:748)
Caused by: org.apache.shiro.authz.UnauthorizedException: Subject does not have permission [DATA:WRITE:helloWorld:1]
  at org.apache.shiro.authz.ModularRealmAuthorizer.checkPermission(ModularRealmAuthorizer.java:334)
  at org.apache.shiro.mgt.AuthorizingSecurityManager.checkPermission(AuthorizingSecurityManager.java:141)
  at org.apache.shiro.subject.support.DelegatingSubject.checkPermission(DelegatingSubject.java:214)
  at org.apache.geode.internal.security.IntegratedSecurityService.authorize(IntegratedSecurityService.java:288)
... 12 more
    
Process finished with exit code 1

The above error message points to an org.apache.geode.security.NotAuthorizedException: appDeveloper not authorized for DATA:WRITE:helloWorld:1, showing that the appDeveloper does not have the correct permissions to write to the cluster.

check-circle-line exclamation-circle-line close-line
Scroll to top icon